Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ba7d208c8 | |||
| 22ff17aec9 | |||
| b2650947a9 | |||
| 604bf0c485 | |||
| b7bf3678e5 | |||
| b0430f7eee | |||
| 7d3e440a47 | |||
| 6877261b9c | |||
| eef45a6015 | |||
| 0aee431527 | |||
| 90a18186d9 | |||
| 38aea7c455 | |||
| e272048f24 | |||
| 6aa9f208ee | |||
| b749bf7b08 | |||
| ff3daed4c6 | |||
| e90e10bd26 | |||
| c6a49b048f | |||
| 29af079a8f | |||
| 9bb6be8e60 | |||
| 226daa990f | |||
| b8e3809452 | |||
| 47881f77d9 | |||
| 69d0a5286e | |||
| ebdd78edea | |||
| eff7c363d4 | |||
| 44cd482695 | |||
| a801e0c5e9 | |||
| 722f836714 | |||
| 1dd62be4ef | |||
| 33f731e247 | |||
| 7cf139f856 | |||
| fd28c939f5 | |||
| 1ab68172cb | |||
| 87e9ae5a3e | |||
| c47a7d78fe | |||
| b10b3a3434 | |||
| 9d4de4b295 | |||
| 2e5b123749 | |||
| 3a6eaa3ddd | |||
| eb42d739cb | |||
| e5d5bd5ec8 | |||
| 24166a4271 | |||
| 232149817e | |||
| a286834eb5 | |||
| c500616bb4 | |||
| 42faa2e75b | |||
| b19cf35d28 | |||
| 4585c7b649 | |||
| 326a2a1877 | |||
| 8d057613f5 | |||
| 0b00c2ad76 | |||
| 310f56a9b3 | |||
| 0a94e740d2 | |||
| 3f3a503def | |||
| 0413865a3b | |||
| 98268a95c2 | |||
| 1110a78e06 | |||
| 6086f76d95 | |||
| 268eaaa9ca | |||
| ad1612d84a | |||
| 0389a58f64 | |||
| 6ee2f334f6 | |||
| a5afdf4e91 | |||
| ecaa570ff3 | |||
| 1d2d1e6df7 | |||
| c242f0079c | |||
| 727c533f93 | |||
| 5961b7d951 | |||
| bbab069bcd | |||
| 1cf3b776d7 | |||
| 1aa2643c18 | |||
| 1150c04298 | |||
| 9c57ab5221 | |||
| 3e61b8c21a | |||
| 99bed645f3 | |||
| 51f5982205 | |||
| b7a06dd0cf | |||
| e071e4cdbf | |||
| 470b18786e | |||
| 8f21453e82 | |||
| fb76917ec3 | |||
| c53500812c | |||
| 5e6b9e578b | |||
| f9c495a596 | |||
| e4ec8cccfd | |||
| 3be350786d | |||
| 7cd43de5d5 | |||
| 0f450fd9bf | |||
| ced8d2a45f | |||
| c59caa5d7f |
@@ -0,0 +1,90 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Information we may share
|
||||||
|
|
||||||
|
We operate our Services using third parties. While we do not share any user data, these third party may access the encrypted user data as it is stored or transmitted via our servers.
|
||||||
|
|
||||||
|
We use Third party to provide email services - if you ask for support via email, your and SimpleX Chat email providers may access these emails according their privacy policies and terms of service.
|
||||||
|
|
||||||
|
The cases when SimpleX Chat may need to share the data we temporarily store on the servers:
|
||||||
|
|
||||||
|
- To meet any applicable law, regulation, legal process or enforceable governmental request.
|
||||||
|
- To enforce applicable Terms, including investigation of potential violations.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
Please also read our Terms of Service.
|
||||||
|
|
||||||
|
If you have questions about our Privacy Policy please contact us at chat@simplex.chat.
|
||||||
|
|
||||||
|
## Terms of Service
|
||||||
|
|
||||||
|
You accept to our Terms of Service ("Terms") by installing or using any of our apps or services ("Services").
|
||||||
|
|
||||||
|
**Minimal age**. You must be at least 13 years old to use our Services. The minimum age to use our Services without parental approval may be higher in your country.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
**Software**. You agree to downloading and installing updates to our Services when they are available; they would only be automatic if you configure your devices in this way.
|
||||||
|
|
||||||
|
**Traffic and device costs**. You are solely responsible for the traffic and device costs on which you use our Services, and any associated taxes.
|
||||||
|
|
||||||
|
**Legal and acceptable usage**. You agree to use our Services only for legal and acceptable purposes. You will not use (or assist others in using) our Services in ways that: 1) violate or infringe the rights of SimpleX Chat, our users, or others, including privacy, publicity, intellectual property, or other proprietary rights; 2) involve sending illegal or impermissible communications, e.g. spam.
|
||||||
|
|
||||||
|
**Damage to SimpleX Chat**. You must not (or assist others to) access, use, modify, distribute, transfer, or exploit our Services in unauthorized manners, or in ways that harm SimpleX Chat, our Services, or systems. For example, you must not 1) access our Services or systems without authorization, other than by using the apps; 2) disrupt the integrity or performance of our Services; 3) collect information about our users in any manner; or 4) sell, rent, or charge for our Services.
|
||||||
|
|
||||||
|
**Keeping your data secure**. SimpleX Chat is the first messaging platform that is 100% private by design - we neither have ability to access your messages, nor we have information about who you communicate with. That means that you are solely responsible for keeping your device and your user profile safe and secure. If you lose your phone or remove the app, you will not be able to recover the lost data, unless you made a back up.
|
||||||
|
|
||||||
|
**Storing the messages on the device**. Currently the messages are stored in the database on your device without encryption. It means that if you make a backup of the app and store it unecrypted, the backup provider may be able to access the messages.
|
||||||
|
|
||||||
|
**No Access to Emergency Services**. Our Services do not provide access to emergency service providers like the police, fire department, hospitals, or other public safety organizations. Make sure you can contact emergency service providers through a mobile, fixed-line telephone, or other service.
|
||||||
|
|
||||||
|
**Third-party services**. Our Services may allow you to access, use, or interact with third-party websites, apps, content, and other products and services. When you use third-party services, their terms and privacy policies govern your use of those services.
|
||||||
|
|
||||||
|
**Your Rights**. You own the mesasges and information you transmit through our Services. Your recipients are able to retain the messages you receive from you; there is no technical ability to delete data from their devices.
|
||||||
|
|
||||||
|
**License**. SimpleX Chat grants you a limited, revocable, non-exclusive, and non-transferable license to use our Services in accordance with these Terms. The source-code of services is available and can be used under [AGPL v3 licence](https://github.com/simplex-chat/simplex-chat/blob/stable/LICENSE)
|
||||||
|
|
||||||
|
**SimpleX Chat Rights**. We own all copyrights, trademarks, domains, logos, trade secrets, and other intellectual property rights associated with our Services. You may not use our copyrights, trademarks, domains, logos, and other intellectual property rights unless you have our written permission, and unless under an open-source license distributed together with the source code. To report copyright, trademark, or other intellectual property infringement, please contact chat@simplex.chat.
|
||||||
|
|
||||||
|
**Disclaimers**. YOU USE OUR SERVICES AT YOUR OWN RISK AND SUBJECT TO THE FOLLOWING DISCLAIMERS. WE PROVIDE OUR SERVICES ON AN “AS IS” BASIS WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT, AND FREEDOM FROM COMPUTER VIRUS OR OTHER HARMFUL CODE. SIMPLEX DOES NOT WARRANT THAT ANY INFORMATION PROVIDED BY US IS ACCURATE, COMPLETE, OR USEFUL, THAT OUR SERVICES WILL BE OPERATIONAL, ERROR-FREE, SECURE, OR SAFE, OR THAT OUR SERVICES WILL FUNCTION WITHOUT DISRUPTIONS, DELAYS, OR IMPERFECTIONS. WE DO NOT CONTROL, AND ARE NOT RESPONSIBLE FOR, CONTROLLING HOW OR WHEN OUR USERS USE OUR SERVICES. WE ARE NOT RESPONSIBLE FOR THE ACTIONS OR INFORMATION (INCLUDING CONTENT) OF OUR USERS OR OTHER THIRD PARTIES. YOU RELEASE US, AFFILIATES, DIRECTORS, OFFICERS, EMPLOYEES, PARTNERS, AND AGENTS ("SIMPLEX PARTIES") FROM ANY CLAIM, COMPLAINT, CAUSE OF ACTION, CONTROVERSY, OR DISPUTE (TOGETHER, "CLAIM") AND DAMAGES, KNOWN AND UNKNOWN, RELATING TO, ARISING OUT OF, OR IN ANY WAY CONNECTED WITH ANY SUCH CLAIM YOU HAVE AGAINST ANY THIRD PARTIES.
|
||||||
|
|
||||||
|
**Limitation of liability**. THE SIMPLEX PARTIES WILL NOT BE LIABLE TO YOU FOR ANY LOST PROFITS OR CONSEQUENTIAL, SPECIAL, PUNITIVE, INDIRECT, OR INCIDENTAL DAMAGES RELATING TO, ARISING OUT OF, OR IN ANY WAY IN CONNECTION WITH OUR TERMS, US, OR OUR SERVICES, EVEN IF THE SIMPLEX PARTIES HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. OUR AGGREGATE LIABILITY RELATING TO, ARISING OUT OF, OR IN ANY WAY IN CONNECTION WITH OUR TERMS, US, OR OUR SERVICES WILL NOT EXCEED ONE DOLLAR ($1). THE FOREGOING DISCLAIMER OF CERTAIN DAMAGES AND LIMITATION OF LIABILITY WILL APPLY TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW. THE LAWS OF SOME JURISDICTIONS MAY NOT ALLOW THE EXCLUSION OR LIMITATION OF CERTAIN DAMAGES, SO SOME OR ALL OF THE EXCLUSIONS AND LIMITATIONS SET FORTH ABOVE MAY NOT APPLY TO YOU. NOTWITHSTANDING ANYTHING TO THE CONTRARY IN OUR TERMS, IN SUCH CASES, THE LIABILITY OF THE SIMPLEX PARTIES WILL BE LIMITED TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||||
|
|
||||||
|
**Availability**. Our Services may be interrupted, including for maintenance, upgrades, or network or equipment failures. We may discontinue some or all of our Services, including certain features and the support for certain devices and platforms, at any time.
|
||||||
|
|
||||||
|
**Resolving disputes**. You agree to resolve any Claim you have with us relating to or arising from our Terms, us, or our Services in the courts of England and Wales. You also agree to submit to the personal jurisdiction of such courts for the purpose of resolving all such disputes. The laws of England govern our Terms, as well as any disputes, whether in court or arbitration, which might arise between SimpleX Chat and you, without regard to conflict of law provisions.
|
||||||
|
|
||||||
|
**Changes to the terms**. SimpleX Chat may update the Terms from time to time. Your continued use of our Services confirms your acceptance of our updated Terms and supersedes any prior Terms. You will comply with all applicable export control and trade sanctions laws. Our Terms cover the entire agreement between you and SimpleX Chat regarding our Services. If you do not agree with our Terms, you should stop using our Services.
|
||||||
|
|
||||||
|
**Enforcing the terms**. If we fail to enforce any of our Terms, that does not mean we waive the right to enforce them. If any provision of the Terms is deemed unlawful, void, or unenforceable, that provision shall be deemed severable from our Terms and shall not affect the enforceability of the remaining provisions. Our Services are not intended for distribution to or use in any country where such distribution or use would violate local law or would subject us to any regulations in another country. We reserve the right to limit our Services in any country. If you have specific questions about these Terms, please contact us at chat@simplex.chat.
|
||||||
|
|
||||||
|
**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
|
||||||
@@ -1,22 +1,30 @@
|
|||||||
<img src="images/simplex-chat-logo.svg" alt="SimpleX logo" width="100%">
|
<img src="images/simplex-chat-logo.svg" alt="SimpleX logo" width="100%">
|
||||||
|
|
||||||
# SimpleX Chat
|
# SimpleX - the first chat platform that is 100% private by design - it has no access to your connection graph!
|
||||||
|
|
||||||
SimpleX - private and secure open-source chat and application platform - public beta for iOS now available!
|
|
||||||
|
|
||||||
[](https://github.com/simplex-chat/simplex-chat/actions?query=workflow%3Abuild)
|
[](https://github.com/simplex-chat/simplex-chat/actions?query=workflow%3Abuild)
|
||||||
[](https://github.com/simplex-chat/simplex-chat/releases)
|
[](https://github.com/simplex-chat/simplex-chat/releases)
|
||||||
[](https://github.com/simplex-chat/simplex-chat/releases)
|
[](https://github.com/simplex-chat/simplex-chat/releases)
|
||||||
[](https://twitter.com/simplexchat)
|
[](https://twitter.com/SimpleXChat)
|
||||||
[](https://www.reddit.com/r/SimpleXChat)
|
[](https://www.reddit.com/r/SimpleXChat)
|
||||||
|
|
||||||
SimpleX Chat apps (both terminal UI and [iOS public beta](https://testflight.apple.com/join/DWuT2LQu)) use [SimpleXMQ](https://github.com/simplex-chat/simplexmq) message broker.
|
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apple_store.svg" alt="iOS app" height="42">](https://apps.apple.com/us/app/simplex-chat/id1605771084)
|
||||||
|
|
||||||
|
[](https://play.google.com/store/apps/details?id=chat.simplex.app)
|
||||||
|
|
||||||
|
[<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="iOS TestFlight" height="41">](https://github.com/simplex-chat/website/raw/master/simplex.apk)
|
||||||
|
|
||||||
|
- 🖲 Protects your messages and metadata - who you talk to and when.
|
||||||
|
- 🔐 Double ratchet encryption.
|
||||||
|
- 📱 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). [See the announcement here](https://github.com/simplex-chat/simplex-chat/blob/master/blog/20220308-simplex-chat-mobile-apps.md).
|
||||||
|
- 🚀 [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](https://github.com/simplex-chat/simplex-chat) on Linux, MacOS, Windows.
|
||||||
|
|
||||||
See [SimpleX overview](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) for more information on platform objectives and technical design.
|
See [SimpleX overview](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) for more information on platform objectives and technical design.
|
||||||
|
|
||||||
***SimpleX Chat [public beta for iOS 15 is available via TestFlight](https://testflight.apple.com/join/DWuT2LQu)** - it will help us a lot if you test it! [See the announcement here](https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20220214-simplex-chat-ios-public-beta.md).*
|
### :zap: Quick installation of a terminal app
|
||||||
|
|
||||||
### :zap: Quick installation
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl -o- https://raw.githubusercontent.com/simplex-chat/simplex-chat/master/install.sh | bash
|
curl -o- https://raw.githubusercontent.com/simplex-chat/simplex-chat/master/install.sh | bash
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<JetCodeStyleSettings>
|
<JetCodeStyleSettings>
|
||||||
<option name="SPACE_BEFORE_EXTEND_COLON" value="false" />
|
<option name="SPACE_BEFORE_EXTEND_COLON" value="false" />
|
||||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="3" />
|
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="3" />
|
||||||
|
<option name="WRAP_EXPRESSION_BODY_FUNCTIONS" value="0" />
|
||||||
<option name="WRAP_ELVIS_EXPRESSIONS" value="0" />
|
<option name="WRAP_ELVIS_EXPRESSIONS" value="0" />
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
</JetCodeStyleSettings>
|
</JetCodeStyleSettings>
|
||||||
@@ -121,10 +122,15 @@
|
|||||||
</codeStyleSettings>
|
</codeStyleSettings>
|
||||||
<codeStyleSettings language="kotlin">
|
<codeStyleSettings language="kotlin">
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
<option name="RIGHT_MARGIN" value="120" />
|
<option name="RIGHT_MARGIN" value="140" />
|
||||||
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
|
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
|
||||||
<option name="KEEP_BLANK_LINES_IN_CODE" value="0" />
|
<option name="KEEP_BLANK_LINES_IN_CODE" value="0" />
|
||||||
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
|
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
|
||||||
|
<option name="CALL_PARAMETERS_WRAP" value="0" />
|
||||||
|
<option name="METHOD_PARAMETERS_WRAP" value="0" />
|
||||||
|
<option name="EXTENDS_LIST_WRAP" value="0" />
|
||||||
|
<option name="METHOD_CALL_CHAIN_WRAP" value="0" />
|
||||||
|
<option name="ASSIGNMENT_WRAP" value="0" />
|
||||||
<option name="METHOD_ANNOTATION_WRAP" value="0" />
|
<option name="METHOD_ANNOTATION_WRAP" value="0" />
|
||||||
<option name="CLASS_ANNOTATION_WRAP" value="0" />
|
<option name="CLASS_ANNOTATION_WRAP" value="0" />
|
||||||
<option name="FIELD_ANNOTATION_WRAP" value="0" />
|
<option name="FIELD_ANNOTATION_WRAP" value="0" />
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "chat.simplex.app"
|
applicationId "chat.simplex.app"
|
||||||
minSdk 26
|
minSdk 29
|
||||||
targetSdk 32
|
targetSdk 32
|
||||||
versionCode 1
|
versionCode 11
|
||||||
versionName "1.0"
|
versionName "1.2"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
ndk {
|
ndk {
|
||||||
@@ -40,6 +40,12 @@ android {
|
|||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = '1.8'
|
jvmTarget = '1.8'
|
||||||
|
freeCompilerArgs += "-opt-in=kotlinx.coroutines.DelicateCoroutinesApi"
|
||||||
|
freeCompilerArgs += "-opt-in=androidx.compose.ui.text.ExperimentalTextApi"
|
||||||
|
freeCompilerArgs += "-opt-in=androidx.compose.material.ExperimentalMaterialApi"
|
||||||
|
freeCompilerArgs += "-opt-in=com.google.accompanist.insets.ExperimentalAnimatedInsets"
|
||||||
|
freeCompilerArgs += "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi"
|
||||||
|
freeCompilerArgs += "-opt-in=kotlinx.serialization.InternalSerializationApi"
|
||||||
}
|
}
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
@@ -66,6 +72,7 @@ dependencies {
|
|||||||
implementation "androidx.compose.material:material:$compose_version"
|
implementation "androidx.compose.material:material:$compose_version"
|
||||||
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
|
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
|
||||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-process:2.4.1'
|
||||||
implementation 'androidx.activity:activity-compose:1.4.0'
|
implementation 'androidx.activity:activity-compose:1.4.0'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.3.2'
|
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.3.2'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2'
|
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2'
|
||||||
@@ -73,6 +80,10 @@ dependencies {
|
|||||||
implementation "androidx.navigation:navigation-compose:2.4.1"
|
implementation "androidx.navigation:navigation-compose:2.4.1"
|
||||||
implementation "com.google.accompanist:accompanist-insets:0.23.0"
|
implementation "com.google.accompanist:accompanist-insets:0.23.0"
|
||||||
|
|
||||||
|
def work_version = "2.7.1"
|
||||||
|
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||||
|
implementation "androidx.work:work-multiprocess:$work_version"
|
||||||
|
|
||||||
def camerax_version = "1.1.0-beta01"
|
def camerax_version = "1.1.0-beta01"
|
||||||
implementation "androidx.camera:camera-core:${camerax_version}"
|
implementation "androidx.camera:camera-core:${camerax_version}"
|
||||||
implementation "androidx.camera:camera-camera2:${camerax_version}"
|
implementation "androidx.camera:camera-camera2:${camerax_version}"
|
||||||
|
|||||||
@@ -11,9 +11,8 @@
|
|||||||
<application
|
<application
|
||||||
android:name="SimplexApp"
|
android:name="SimplexApp"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/icon"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.SimpleX">
|
android:theme="@style/Theme.SimpleX">
|
||||||
<activity
|
<activity
|
||||||
|
|||||||
|
After Width: | Height: | Size: 52 KiB |
@@ -2,159 +2,119 @@ package chat.simplex.app
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.navigation.*
|
|
||||||
import androidx.navigation.compose.*
|
|
||||||
import chat.simplex.app.model.ChatModel
|
import chat.simplex.app.model.ChatModel
|
||||||
|
import chat.simplex.app.model.NtfManager
|
||||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||||
import chat.simplex.app.views.*
|
import chat.simplex.app.views.SplashView
|
||||||
import chat.simplex.app.views.chat.ChatInfoView
|
import chat.simplex.app.views.WelcomeView
|
||||||
import chat.simplex.app.views.chat.ChatView
|
import chat.simplex.app.views.chat.ChatView
|
||||||
import chat.simplex.app.views.chatlist.ChatListView
|
import chat.simplex.app.views.chatlist.ChatListView
|
||||||
|
import chat.simplex.app.views.chatlist.openChat
|
||||||
|
import chat.simplex.app.views.helpers.AlertManager
|
||||||
import chat.simplex.app.views.helpers.withApi
|
import chat.simplex.app.views.helpers.withApi
|
||||||
import chat.simplex.app.views.newchat.*
|
import chat.simplex.app.views.newchat.*
|
||||||
import chat.simplex.app.views.usersettings.SettingsView
|
|
||||||
import chat.simplex.app.views.usersettings.UserProfileView
|
|
||||||
import com.google.accompanist.insets.ExperimentalAnimatedInsets
|
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
//import kotlinx.serialization.decodeFromString
|
||||||
@ExperimentalAnimatedInsets
|
|
||||||
@ExperimentalPermissionsApi
|
|
||||||
@ExperimentalMaterialApi
|
|
||||||
class MainActivity: ComponentActivity() {
|
class MainActivity: ComponentActivity() {
|
||||||
private val vm by viewModels<SimplexViewModel>()
|
private val vm by viewModels<SimplexViewModel>()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
connectIfOpenedViaUri(intent, vm.chatModel)
|
// testJson()
|
||||||
|
processIntent(intent, vm.chatModel)
|
||||||
|
// vm.app.initiateBackgroundWork()
|
||||||
setContent {
|
setContent {
|
||||||
SimpleXTheme {
|
SimpleXTheme {
|
||||||
Navigation(vm.chatModel)
|
Surface(
|
||||||
}
|
Modifier
|
||||||
}
|
.background(MaterialTheme.colors.background)
|
||||||
}
|
.fillMaxSize()
|
||||||
}
|
) {
|
||||||
|
MainPage(vm.chatModel)
|
||||||
@DelicateCoroutinesApi
|
|
||||||
class SimplexViewModel(application: Application): AndroidViewModel(application) {
|
|
||||||
val chatModel = getApplication<SimplexApp>().chatModel
|
|
||||||
}
|
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
|
||||||
@ExperimentalPermissionsApi
|
|
||||||
@ExperimentalMaterialApi
|
|
||||||
@Composable
|
|
||||||
fun MainPage(chatModel: ChatModel, nav: NavController) {
|
|
||||||
when (chatModel.userCreated.value) {
|
|
||||||
null -> SplashView()
|
|
||||||
false -> WelcomeView(chatModel) { nav.navigate(Pages.ChatList.route) }
|
|
||||||
true -> ChatListView(chatModel, nav)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExperimentalAnimatedInsets
|
|
||||||
@DelicateCoroutinesApi
|
|
||||||
@ExperimentalPermissionsApi
|
|
||||||
@ExperimentalMaterialApi
|
|
||||||
@Composable
|
|
||||||
fun Navigation(chatModel: ChatModel) {
|
|
||||||
val nav = rememberNavController()
|
|
||||||
|
|
||||||
Box {
|
|
||||||
NavHost(navController = nav, startDestination = Pages.Home.route) {
|
|
||||||
composable(route = Pages.Home.route) {
|
|
||||||
MainPage(chatModel, nav)
|
|
||||||
}
|
|
||||||
composable(route = Pages.Welcome.route) {
|
|
||||||
WelcomeView(chatModel) {
|
|
||||||
nav.navigate(Pages.Home.route) {
|
|
||||||
popUpTo(Pages.Home.route) { inclusive = true }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable(route = Pages.ChatList.route) {
|
|
||||||
ChatListView(chatModel, nav)
|
|
||||||
}
|
|
||||||
composable(route = Pages.Chat.route) {
|
|
||||||
ChatView(chatModel, nav)
|
|
||||||
}
|
|
||||||
composable(route = Pages.AddContact.route) {
|
|
||||||
AddContactView(chatModel, nav)
|
|
||||||
}
|
|
||||||
composable(route = Pages.Connect.route) {
|
|
||||||
ConnectContactView(chatModel, nav)
|
|
||||||
}
|
|
||||||
composable(route = Pages.ChatInfo.route) {
|
|
||||||
ChatInfoView(chatModel, nav)
|
|
||||||
}
|
|
||||||
composable(route = Pages.Terminal.route) {
|
|
||||||
TerminalView(chatModel, nav)
|
|
||||||
}
|
|
||||||
composable(
|
|
||||||
Pages.TerminalItemDetails.route + "/{identifier}",
|
|
||||||
arguments = listOf(
|
|
||||||
navArgument("identifier") {
|
|
||||||
type = NavType.LongType
|
|
||||||
}
|
|
||||||
)
|
|
||||||
) { entry -> DetailView(entry.arguments!!.getLong("identifier"), chatModel.terminalItems, nav) }
|
|
||||||
composable(route = Pages.Settings.route) {
|
|
||||||
SettingsView(chatModel, nav)
|
|
||||||
}
|
|
||||||
composable(route = Pages.UserProfile.route) {
|
|
||||||
UserProfileView(chatModel, nav)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val am = chatModel.alertManager
|
|
||||||
if (am.presentAlert.value) am.alertView.value?.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class Pages(val route: String) {
|
|
||||||
object Home: Pages("home")
|
|
||||||
object Terminal: Pages("terminal")
|
|
||||||
object Welcome: Pages("welcome")
|
|
||||||
object TerminalItemDetails: Pages("details")
|
|
||||||
object ChatList: Pages("chats")
|
|
||||||
object Chat: Pages("chat")
|
|
||||||
object AddContact: Pages("add_contact")
|
|
||||||
object Connect: Pages("connect")
|
|
||||||
object ChatInfo: Pages("chat_info")
|
|
||||||
object Settings: Pages("settings")
|
|
||||||
object UserProfile: Pages("user_profile")
|
|
||||||
}
|
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
|
||||||
fun connectIfOpenedViaUri(intent: Intent?, chatModel: ChatModel) {
|
|
||||||
val uri = intent?.data
|
|
||||||
if (intent?.action == "android.intent.action.VIEW" && uri != null) {
|
|
||||||
Log.d("SIMPLEX", "connectIfOpenedViaUri: opened via link")
|
|
||||||
if (chatModel.currentUser.value == null) {
|
|
||||||
chatModel.appOpenUrl.value = uri
|
|
||||||
} else {
|
|
||||||
withUriAction(chatModel, uri) { action ->
|
|
||||||
chatModel.alertManager.showAlertMsg(
|
|
||||||
title = "Connect via $action link?",
|
|
||||||
text = "Your profile will be sent to the contact that you received this link from.",
|
|
||||||
confirmText = "Connect",
|
|
||||||
onConfirm = {
|
|
||||||
withApi {
|
|
||||||
Log.d("SIMPLEX", "connectIfOpenedViaUri: connecting")
|
|
||||||
connectViaUri(chatModel, action, uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SimplexViewModel(application: Application): AndroidViewModel(application) {
|
||||||
|
val app = getApplication<SimplexApp>()
|
||||||
|
val chatModel = app.chatModel
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MainPage(chatModel: ChatModel) {
|
||||||
|
Box {
|
||||||
|
when (chatModel.userCreated.value) {
|
||||||
|
null -> SplashView()
|
||||||
|
false -> WelcomeView(chatModel)
|
||||||
|
true ->
|
||||||
|
if (chatModel.chatId.value == null) ChatListView(chatModel)
|
||||||
|
else ChatView(chatModel)
|
||||||
|
}
|
||||||
|
ModalManager.shared.showInView()
|
||||||
|
AlertManager.shared.showInView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun processIntent(intent: Intent?, chatModel: ChatModel) {
|
||||||
|
when (intent?.action) {
|
||||||
|
NtfManager.OpenChatAction -> {
|
||||||
|
val chatId = intent.getStringExtra("chatId")
|
||||||
|
Log.d(TAG, "processIntent: OpenChatAction $chatId")
|
||||||
|
if (chatId != null) {
|
||||||
|
val cInfo = chatModel.getChat(chatId)?.chatInfo
|
||||||
|
if (cInfo != null) withApi { openChat(chatModel, cInfo) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"android.intent.action.VIEW" -> {
|
||||||
|
val uri = intent.data
|
||||||
|
if (uri != null) connectIfOpenedViaUri(uri, chatModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) {
|
||||||
|
Log.d(TAG, "connectIfOpenedViaUri: opened via link")
|
||||||
|
if (chatModel.currentUser.value == null) {
|
||||||
|
// TODO open from chat list view
|
||||||
|
chatModel.appOpenUrl.value = uri
|
||||||
|
} else {
|
||||||
|
withUriAction(uri) { action ->
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
title = "Connect via $action link?",
|
||||||
|
text = "Your profile will be sent to the contact that you received this link from.",
|
||||||
|
confirmText = "Connect",
|
||||||
|
onConfirm = {
|
||||||
|
withApi {
|
||||||
|
Log.d(TAG, "connectIfOpenedViaUri: connecting")
|
||||||
|
connectViaUri(chatModel, action, uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//fun testJson() {
|
||||||
|
// val str = """
|
||||||
|
// {}
|
||||||
|
// """.trimIndent()
|
||||||
|
//
|
||||||
|
// println(json.decodeFromString<ChatItem>(str))
|
||||||
|
//}
|
||||||
|
|||||||
@@ -3,19 +3,16 @@ package chat.simplex.app
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.net.LocalServerSocket
|
import android.net.LocalServerSocket
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.material.*
|
import chat.simplex.app.model.*
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import chat.simplex.app.model.ChatController
|
|
||||||
import chat.simplex.app.model.ChatModel
|
|
||||||
import chat.simplex.app.views.helpers.withApi
|
import chat.simplex.app.views.helpers.withApi
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.Semaphore
|
import java.util.concurrent.Semaphore
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
const val TAG = "SIMPLEX"
|
||||||
|
|
||||||
// ghc's rts
|
// ghc's rts
|
||||||
external fun initHS()
|
external fun initHS()
|
||||||
// android-support
|
// android-support
|
||||||
@@ -27,15 +24,18 @@ external fun chatInit(path: String): ChatCtrl
|
|||||||
external fun chatSendCmd(ctrl: ChatCtrl, msg: String) : String
|
external fun chatSendCmd(ctrl: ChatCtrl, msg: String) : String
|
||||||
external fun chatRecvMsg(ctrl: ChatCtrl) : String
|
external fun chatRecvMsg(ctrl: ChatCtrl) : String
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
//class SimplexApp: Application(), LifecycleEventObserver {
|
||||||
class SimplexApp: Application() {
|
class SimplexApp: Application() {
|
||||||
private lateinit var controller: ChatController
|
private lateinit var controller: ChatController
|
||||||
lateinit var chatModel: ChatModel
|
lateinit var chatModel: ChatModel
|
||||||
|
private lateinit var ntfManager: NtfManager
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
// ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||||
|
ntfManager = NtfManager(applicationContext)
|
||||||
val ctrl = chatInit(applicationContext.filesDir.toString())
|
val ctrl = chatInit(applicationContext.filesDir.toString())
|
||||||
controller = ChatController(ctrl, AlertManager())
|
controller = ChatController(ctrl, ntfManager, applicationContext)
|
||||||
chatModel = controller.chatModel
|
chatModel = controller.chatModel
|
||||||
withApi {
|
withApi {
|
||||||
val user = controller.apiGetActiveUser()
|
val user = controller.apiGetActiveUser()
|
||||||
@@ -43,39 +43,13 @@ class SimplexApp: Application() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AlertManager {
|
// override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||||
var alertView = mutableStateOf<(@Composable () -> Unit)?>(null)
|
// Log.d(TAG, "onStateChanged: $event")
|
||||||
var presentAlert = mutableStateOf<Boolean>(false)
|
// if (event == Lifecycle.Event.ON_STOP) {
|
||||||
|
// Log.e(TAG, "BGManager schedule ${Clock.System.now()}")
|
||||||
fun showAlert(alert: @Composable () -> Unit) {
|
// BGManager.schedule(applicationContext)
|
||||||
Log.d("SIMPLEX", "AlertManager.showAlert")
|
// }
|
||||||
alertView.value = alert
|
// }
|
||||||
presentAlert.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hideAlert() {
|
|
||||||
presentAlert.value = false
|
|
||||||
alertView.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showAlertMsg(title: String, text: String? = null,
|
|
||||||
confirmText: String = "Ok", onConfirm: (() -> Unit)? = null) {
|
|
||||||
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
|
|
||||||
showAlert {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = this::hideAlert,
|
|
||||||
title = { Text(title) },
|
|
||||||
text = alertText,
|
|
||||||
confirmButton = {
|
|
||||||
Button(onClick = {
|
|
||||||
onConfirm?.invoke()
|
|
||||||
hideAlert()
|
|
||||||
}) { Text(confirmText) }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
init {
|
init {
|
||||||
@@ -83,12 +57,12 @@ class SimplexApp: Application() {
|
|||||||
|
|
||||||
val s = Semaphore(0)
|
val s = Semaphore(0)
|
||||||
thread(name="stdout/stderr pipe") {
|
thread(name="stdout/stderr pipe") {
|
||||||
Log.d("SIMPLEX", "starting server")
|
Log.d(TAG, "starting server")
|
||||||
val server = LocalServerSocket(socketName)
|
val server = LocalServerSocket(socketName)
|
||||||
Log.d("SIMPLEX", "started server")
|
Log.d(TAG, "started server")
|
||||||
s.release()
|
s.release()
|
||||||
val receiver = server.accept()
|
val receiver = server.accept()
|
||||||
Log.d("SIMPLEX", "started receiver")
|
Log.d(TAG, "started receiver")
|
||||||
val logbuffer = FifoQueue<String>(500)
|
val logbuffer = FifoQueue<String>(500)
|
||||||
if (receiver != null) {
|
if (receiver != null) {
|
||||||
val inStream = receiver.inputStream
|
val inStream = receiver.inputStream
|
||||||
@@ -97,7 +71,7 @@ class SimplexApp: Application() {
|
|||||||
|
|
||||||
while(true) {
|
while(true) {
|
||||||
val line = input.readLine() ?: break
|
val line = input.readLine() ?: break
|
||||||
Log.d("SIMPLEX (stdout/stderr)", line)
|
Log.w("$TAG (stdout/stderr)", line)
|
||||||
logbuffer.add(line)
|
logbuffer.add(line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package chat.simplex.app.model
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.work.*
|
||||||
|
import chat.simplex.app.TAG
|
||||||
|
import chat.simplex.app.chatRecvMsg
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import java.time.Duration
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class BGManager(appContext: Context, workerParams: WorkerParameters): //, ctrl: ChatCtrl):
|
||||||
|
Worker(appContext, workerParams) {
|
||||||
|
// val controller = ctrl
|
||||||
|
|
||||||
|
init {}
|
||||||
|
|
||||||
|
override fun doWork(): Result {
|
||||||
|
Log.e(TAG, "BGManager doWork ${Clock.System.now()}")
|
||||||
|
schedule(applicationContext)
|
||||||
|
getNewItems()
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getNewItems() {
|
||||||
|
Log.e(TAG, "BGManager getNewItems")
|
||||||
|
// val json = chatRecvMsg(controller)
|
||||||
|
// val r = APIResponse.decodeStr(json).resp
|
||||||
|
// Log.d(TAG, "chatRecvMsg: ${r.responseType}")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val constraints = Constraints.Builder()
|
||||||
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun schedule(appContext: Context) {
|
||||||
|
val request = OneTimeWorkRequestBuilder<BGManager>()
|
||||||
|
.setInitialDelay(Duration.ofMinutes(10))
|
||||||
|
.setConstraints(constraints)
|
||||||
|
.build()
|
||||||
|
WorkManager.getInstance(appContext)
|
||||||
|
.enqueue(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,22 @@
|
|||||||
package chat.simplex.app.model
|
package chat.simplex.app.model
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.*
|
||||||
import chat.simplex.app.SimplexApp
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.font.*
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import chat.simplex.app.ui.theme.SecretColor
|
||||||
|
import chat.simplex.app.ui.theme.SimplexBlue
|
||||||
import kotlinx.datetime.*
|
import kotlinx.datetime.*
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.*
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.descriptors.*
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.*
|
||||||
|
|
||||||
class ChatModel(val controller: ChatController, val alertManager: SimplexApp.AlertManager) {
|
class ChatModel(val controller: ChatController) {
|
||||||
var currentUser = mutableStateOf<User?>(null)
|
var currentUser = mutableStateOf<User?>(null)
|
||||||
var userCreated = mutableStateOf<Boolean?>(null)
|
var userCreated = mutableStateOf<Boolean?>(null)
|
||||||
var chats = mutableStateListOf<Chat>()
|
var chats = mutableStateListOf<Chat>()
|
||||||
@@ -17,6 +25,8 @@ class ChatModel(val controller: ChatController, val alertManager: SimplexApp.Ale
|
|||||||
|
|
||||||
var connReqInvitation: String? = null
|
var connReqInvitation: String? = null
|
||||||
var terminalItems = mutableStateListOf<TerminalItem>()
|
var terminalItems = mutableStateListOf<TerminalItem>()
|
||||||
|
var userAddress = mutableStateOf<String?>(null)
|
||||||
|
var userSMPServers = mutableStateOf<(List<String>)?>(null)
|
||||||
// set when app is opened via contact or invitation URI
|
// set when app is opened via contact or invitation URI
|
||||||
var appOpenUrl = mutableStateOf<Uri?>(null)
|
var appOpenUrl = mutableStateOf<Uri?>(null)
|
||||||
|
|
||||||
@@ -54,14 +64,15 @@ class ChatModel(val controller: ChatController, val alertManager: SimplexApp.Ale
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// func replaceChat(_ id: String, _ chat: Chat) {
|
fun replaceChat(id: String, chat: Chat) {
|
||||||
// if let i = getChatIndex(id) {
|
val i = getChatIndex(id)
|
||||||
// chats[i] = chat
|
if (i >= 0) {
|
||||||
// } else {
|
chats[i] = chat
|
||||||
// // invalid state, correcting
|
} else {
|
||||||
// chats.insert(chat, at: 0)
|
// invalid state, correcting
|
||||||
// }
|
chats.add(index = 0, chat)
|
||||||
// }
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun addChatItem(cInfo: ChatInfo, cItem: ChatItem) {
|
fun addChatItem(cInfo: ChatInfo, cItem: ChatItem) {
|
||||||
// update previews
|
// update previews
|
||||||
@@ -240,6 +251,13 @@ data class Chat (
|
|||||||
@Serializable @SerialName("disconnected") class Disconnected: NetworkStatus()
|
@Serializable @SerialName("disconnected") class Disconnected: NetworkStatus()
|
||||||
@Serializable @SerialName("error") class Error(val error: String): NetworkStatus()
|
@Serializable @SerialName("error") class Error(val error: String): NetworkStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val sampleData = Chat(
|
||||||
|
chatInfo = ChatInfo.Direct.sampleData,
|
||||||
|
chatItems = arrayListOf(ChatItem.getSampleData())
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -320,6 +338,12 @@ class Contact(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ContactSubStatus(
|
||||||
|
val contact: Contact,
|
||||||
|
val contactError: ChatError? = null
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Connection(val connStatus: String) {
|
class Connection(val connStatus: String) {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -401,6 +425,12 @@ class GroupMember (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class MemberSubError (
|
||||||
|
val member: GroupMember,
|
||||||
|
val memberError: ChatError
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class UserContactRequest (
|
class UserContactRequest (
|
||||||
val contactRequestId: Long,
|
val contactRequestId: Long,
|
||||||
@@ -435,14 +465,21 @@ class AChatItem (
|
|||||||
data class ChatItem (
|
data class ChatItem (
|
||||||
val chatDir: CIDirection,
|
val chatDir: CIDirection,
|
||||||
val meta: CIMeta,
|
val meta: CIMeta,
|
||||||
val content: CIContent
|
val content: CIContent,
|
||||||
|
val formattedText: List<FormattedText>? = null
|
||||||
) {
|
) {
|
||||||
val id: Long get() = meta.itemId
|
val id: Long get() = meta.itemId
|
||||||
val timestampText: String get() = meta.timestampText
|
val timestampText: String get() = meta.timestampText
|
||||||
val isRcvNew: Boolean get() = meta.itemStatus is CIStatus.RcvNew
|
val isRcvNew: Boolean get() = meta.itemStatus is CIStatus.RcvNew
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getSampleData(id: Long, dir: CIDirection, ts: Instant, text: String,status: CIStatus = CIStatus.SndNew()) =
|
fun getSampleData(
|
||||||
|
id: Long = 1,
|
||||||
|
dir: CIDirection = CIDirection.DirectSnd(),
|
||||||
|
ts: Instant = Clock.System.now(),
|
||||||
|
text: String = "hello\nthere",
|
||||||
|
status: CIStatus = CIStatus.SndNew()
|
||||||
|
) =
|
||||||
ChatItem(
|
ChatItem(
|
||||||
chatDir = dir,
|
chatDir = dir,
|
||||||
meta = CIMeta.getSample(id, ts, text, status),
|
meta = CIMeta.getSample(id, ts, text, status),
|
||||||
@@ -554,18 +591,121 @@ sealed class CIContent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable(with = MsgContentSerializer::class)
|
||||||
sealed class MsgContent {
|
sealed class MsgContent {
|
||||||
abstract val text: String
|
abstract val text: String
|
||||||
abstract val cmdString: String
|
|
||||||
|
|
||||||
@Serializable @SerialName("text")
|
class MCText(override val text: String): MsgContent()
|
||||||
class MCText(override val text: String): MsgContent() {
|
class MCUnknown(val type: String? = null, override val text: String, val json: JsonElement): MsgContent()
|
||||||
override val cmdString get() = "text $text"
|
|
||||||
|
val cmdString: String get() = when (this) {
|
||||||
|
is MCText -> "text $text"
|
||||||
|
is MCUnknown -> "json $json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object MsgContentSerializer : KSerializer<MsgContent> {
|
||||||
|
override val descriptor: SerialDescriptor = buildSerialDescriptor("MsgContent", PolymorphicKind.SEALED) {
|
||||||
|
element("MCText", buildClassSerialDescriptor("MCText") {
|
||||||
|
element<String>("text")
|
||||||
|
})
|
||||||
|
element("MCUnknown", buildClassSerialDescriptor("MCUnknown"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): MsgContent {
|
||||||
|
require(decoder is JsonDecoder)
|
||||||
|
val json = decoder.decodeJsonElement()
|
||||||
|
return if (json is JsonObject) {
|
||||||
|
if ("type" in json) {
|
||||||
|
val t = json["type"]?.jsonPrimitive?.content ?: ""
|
||||||
|
val text = json["text"]?.jsonPrimitive?.content ?: "unknown message format"
|
||||||
|
when (t) {
|
||||||
|
"text" -> MsgContent.MCText(text)
|
||||||
|
else -> MsgContent.MCUnknown(t, text, json)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
MsgContent.MCUnknown(text = "invalid message format", json = json)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
MsgContent.MCUnknown(text = "invalid message format", json = json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: MsgContent) {
|
||||||
|
require(encoder is JsonEncoder)
|
||||||
|
val json = when (value) {
|
||||||
|
is MsgContent.MCText ->
|
||||||
|
buildJsonObject {
|
||||||
|
put("type", "text")
|
||||||
|
put("text", value.text)
|
||||||
|
}
|
||||||
|
is MsgContent.MCUnknown -> value.json
|
||||||
|
}
|
||||||
|
encoder.encodeJsonElement(json)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class RcvFileTransfer {
|
class FormattedText(val text: String, val format: Format? = null) {
|
||||||
|
val link: String? = when (format) {
|
||||||
|
is Format.Uri -> text
|
||||||
|
is Format.Email -> "mailto:$text"
|
||||||
|
is Format.Phone -> "tel:$text"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed class Format {
|
||||||
|
@Serializable @SerialName("bold") class Bold: Format()
|
||||||
|
@Serializable @SerialName("italic") class Italic: Format()
|
||||||
|
@Serializable @SerialName("strikeThrough") class StrikeThrough: Format()
|
||||||
|
@Serializable @SerialName("snippet") class Snippet: Format()
|
||||||
|
@Serializable @SerialName("secret") class Secret: Format()
|
||||||
|
@Serializable @SerialName("colored") class Colored(val color: FormatColor): Format()
|
||||||
|
@Serializable @SerialName("uri") class Uri: Format()
|
||||||
|
@Serializable @SerialName("email") class Email: Format()
|
||||||
|
@Serializable @SerialName("phone") class Phone: Format()
|
||||||
|
|
||||||
|
val style: SpanStyle @Composable get() = when (this) {
|
||||||
|
is Bold -> SpanStyle(fontWeight = FontWeight.Bold)
|
||||||
|
is Italic -> SpanStyle(fontStyle = FontStyle.Italic)
|
||||||
|
is StrikeThrough -> SpanStyle(textDecoration = TextDecoration.LineThrough)
|
||||||
|
is Snippet -> SpanStyle(fontFamily = FontFamily.Monospace)
|
||||||
|
is Secret -> SpanStyle(color = Color.Transparent, background = SecretColor)
|
||||||
|
is Colored -> SpanStyle(color = this.color.uiColor)
|
||||||
|
is Uri -> linkStyle
|
||||||
|
is Email -> linkStyle
|
||||||
|
is Phone -> linkStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val linkStyle @Composable get() = SpanStyle(color = MaterialTheme.colors.primary, textDecoration = TextDecoration.Underline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class FormatColor(val color: String) {
|
||||||
|
red("red"),
|
||||||
|
green("green"),
|
||||||
|
blue("blue"),
|
||||||
|
yellow("yellow"),
|
||||||
|
cyan("cyan"),
|
||||||
|
magenta("magenta"),
|
||||||
|
black("black"),
|
||||||
|
white("white");
|
||||||
|
|
||||||
|
val uiColor: Color @Composable get() = when (this) {
|
||||||
|
red -> Color.Red
|
||||||
|
green -> Color.Green
|
||||||
|
blue -> SimplexBlue
|
||||||
|
yellow -> Color.Yellow
|
||||||
|
cyan -> Color.Cyan
|
||||||
|
magenta -> Color.Magenta
|
||||||
|
black -> MaterialTheme.colors.onBackground
|
||||||
|
white -> MaterialTheme.colors.onBackground
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class RcvFileTransfer
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package chat.simplex.app.model
|
||||||
|
|
||||||
|
import android.app.*
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import chat.simplex.app.*
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
|
||||||
|
class NtfManager(val context: Context) {
|
||||||
|
companion object {
|
||||||
|
const val MessageChannel: String = "chat.simplex.app.MESSAGE_NOTIFICATION"
|
||||||
|
const val MessageGroup: String = "chat.simplex.app.MESSAGE_NOTIFICATION"
|
||||||
|
const val OpenChatAction: String = "chat.simplex.app.OPEN_CHAT"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val manager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
private var prevNtfTime = mutableMapOf<String, Long>()
|
||||||
|
private val msgNtfTimeoutMs = 30000L
|
||||||
|
|
||||||
|
init {
|
||||||
|
manager.createNotificationChannel(NotificationChannel(
|
||||||
|
MessageChannel,
|
||||||
|
"SimpleX Chat messages",
|
||||||
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notifyMessageReceived(cInfo: ChatInfo, cItem: ChatItem) {
|
||||||
|
Log.d(TAG, "notifyMessageReceived ${cInfo.id}")
|
||||||
|
val now = Clock.System.now().toEpochMilliseconds()
|
||||||
|
val recentNotification = (now - prevNtfTime.getOrDefault(cInfo.id, 0) < msgNtfTimeoutMs)
|
||||||
|
prevNtfTime[cInfo.id] = now
|
||||||
|
|
||||||
|
val notification = NotificationCompat.Builder(context, MessageChannel)
|
||||||
|
.setContentTitle(cInfo.displayName)
|
||||||
|
.setContentText(cItem.content.text)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setGroup(MessageGroup)
|
||||||
|
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
|
||||||
|
.setSmallIcon(R.drawable.ntf_icon)
|
||||||
|
.setColor(0x88FFFF)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setContentIntent(getMsgPendingIntent(cInfo))
|
||||||
|
.setSilent(recentNotification)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val summary = NotificationCompat.Builder(context, MessageChannel)
|
||||||
|
.setSmallIcon(R.drawable.ntf_icon)
|
||||||
|
.setColor(0x88FFFF)
|
||||||
|
.setGroup(MessageGroup)
|
||||||
|
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
|
||||||
|
.setGroupSummary(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
with(NotificationManagerCompat.from(context)) {
|
||||||
|
// using cInfo.id only shows one notification per chat and updates it when the message arrives
|
||||||
|
notify(cInfo.id.hashCode(), notification)
|
||||||
|
notify(0, summary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMsgPendingIntent(cInfo: ChatInfo) : PendingIntent{
|
||||||
|
Log.d(TAG, "getMsgPendingIntent ${cInfo.id}")
|
||||||
|
val uniqueInt = (System.currentTimeMillis() and 0xfffffff).toInt()
|
||||||
|
val intent = Intent(context, MainActivity::class.java)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
|
.putExtra("chatId", cInfo.id)
|
||||||
|
.setAction(OpenChatAction)
|
||||||
|
return TaskStackBuilder.create(context).run {
|
||||||
|
addNextIntentWithParentStack(intent)
|
||||||
|
getPendingIntent(uniqueInt, PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
package chat.simplex.app.model
|
package chat.simplex.app.model
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.app.ActivityManager.RunningAppProcessInfo
|
||||||
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import chat.simplex.app.*
|
import chat.simplex.app.*
|
||||||
import kotlinx.coroutines.*
|
import chat.simplex.app.views.helpers.AlertManager
|
||||||
|
import chat.simplex.app.views.helpers.withApi
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.*
|
||||||
@@ -13,36 +19,42 @@ import kotlin.concurrent.thread
|
|||||||
|
|
||||||
typealias ChatCtrl = Long
|
typealias ChatCtrl = Long
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val appContext: Context) {
|
||||||
open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.AlertManager) {
|
var chatModel = ChatModel(this)
|
||||||
var chatModel = ChatModel(this, alertManager)
|
|
||||||
|
|
||||||
suspend fun startChat(u: User) {
|
suspend fun startChat(u: User) {
|
||||||
chatModel.currentUser = mutableStateOf(u)
|
Log.d(TAG, "user: $u")
|
||||||
chatModel.userCreated.value = true
|
|
||||||
Log.d("SIMPLEX (user)", u.toString())
|
|
||||||
try {
|
try {
|
||||||
apiStartChat()
|
apiStartChat()
|
||||||
|
chatModel.userAddress.value = apiGetUserAddress()
|
||||||
|
chatModel.userSMPServers.value = getUserSMPServers()
|
||||||
chatModel.chats.addAll(apiGetChats())
|
chatModel.chats.addAll(apiGetChats())
|
||||||
|
chatModel.currentUser = mutableStateOf(u)
|
||||||
|
chatModel.userCreated.value = true
|
||||||
startReceiver()
|
startReceiver()
|
||||||
Log.d("SIMPLEX", "started chat")
|
Log.d(TAG, "started chat")
|
||||||
} catch(e: Error) {
|
} catch(e: Error) {
|
||||||
Log.d("SIMPLEX", "failed starting chat $e")
|
Log.e(TAG, "failed starting chat $e")
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startReceiver() {
|
fun startReceiver() {
|
||||||
thread(name="receiver") {
|
thread(name="receiver") {
|
||||||
// val chatlog = FifoQueue<String>(500)
|
withApi { recvMspLoop() }
|
||||||
while(true) {
|
}
|
||||||
val json = chatRecvMsg(ctrl)
|
}
|
||||||
val r = APIResponse.decodeStr(json).resp
|
|
||||||
Log.d("SIMPLEX", "chatRecvMsg: ${r.responseType}")
|
open fun isAppOnForeground(context: Context): Boolean {
|
||||||
if (r is CR.Response || r is CR.Invalid) Log.d("SIMPLEX", "chatRecvMsg json: $json")
|
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||||
processReceivedMsg(r)
|
val appProcesses = activityManager.runningAppProcesses ?: return false
|
||||||
|
val packageName = context.packageName
|
||||||
|
for (appProcess in appProcesses) {
|
||||||
|
if (appProcess.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND && appProcess.processName == packageName) {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendCmd(cmd: CC): CR {
|
suspend fun sendCmd(cmd: CC): CR {
|
||||||
@@ -50,21 +62,36 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert
|
|||||||
val c = cmd.cmdString
|
val c = cmd.cmdString
|
||||||
chatModel.terminalItems.add(TerminalItem.cmd(cmd))
|
chatModel.terminalItems.add(TerminalItem.cmd(cmd))
|
||||||
val json = chatSendCmd(ctrl, c)
|
val json = chatSendCmd(ctrl, c)
|
||||||
Log.d("SIMPLEX", "sendCmd: ${cmd.cmdType}")
|
Log.d(TAG, "sendCmd: ${cmd.cmdType}")
|
||||||
val r = APIResponse.decodeStr(json)
|
val r = APIResponse.decodeStr(json)
|
||||||
Log.d("SIMPLEX", "sendCmd response type ${r.resp.responseType}")
|
Log.d(TAG, "sendCmd response type ${r.resp.responseType}")
|
||||||
if (r.resp is CR.Response || r.resp is CR.Invalid) {
|
if (r.resp is CR.Response || r.resp is CR.Invalid) {
|
||||||
Log.d("SIMPLEX", "sendCmd response json $json")
|
Log.d(TAG, "sendCmd response json $json")
|
||||||
}
|
}
|
||||||
chatModel.terminalItems.add(TerminalItem.resp(r.resp))
|
chatModel.terminalItems.add(TerminalItem.resp(r.resp))
|
||||||
r.resp
|
r.resp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun recvMsg(): CR {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val json = chatRecvMsg(ctrl)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun recvMspLoop() {
|
||||||
|
processReceivedMsg(recvMsg())
|
||||||
|
recvMspLoop()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun apiGetActiveUser(): User? {
|
suspend fun apiGetActiveUser(): User? {
|
||||||
val r = sendCmd(CC.ShowActiveUser())
|
val r = sendCmd(CC.ShowActiveUser())
|
||||||
if (r is CR.ActiveUser) return r.user
|
if (r is CR.ActiveUser) return r.user
|
||||||
Log.d("SIMPLEX", "apiGetActiveUser: ${r.responseType} ${r.details}")
|
Log.d(TAG, "apiGetActiveUser: ${r.responseType} ${r.details}")
|
||||||
chatModel.userCreated.value = false
|
chatModel.userCreated.value = false
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -72,7 +99,7 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert
|
|||||||
suspend fun apiCreateActiveUser(p: Profile): User {
|
suspend fun apiCreateActiveUser(p: Profile): User {
|
||||||
val r = sendCmd(CC.CreateActiveUser(p))
|
val r = sendCmd(CC.CreateActiveUser(p))
|
||||||
if (r is CR.ActiveUser) return r.user
|
if (r is CR.ActiveUser) return r.user
|
||||||
Log.d("SIMPLEX", "apiCreateActiveUser: ${r.responseType} ${r.details}")
|
Log.d(TAG, "apiCreateActiveUser: ${r.responseType} ${r.details}")
|
||||||
throw Error("user not created ${r.responseType} ${r.details}")
|
throw Error("user not created ${r.responseType} ${r.details}")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,21 +118,43 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert
|
|||||||
suspend fun apiGetChat(type: ChatType, id: Long): Chat? {
|
suspend fun apiGetChat(type: ChatType, id: Long): Chat? {
|
||||||
val r = sendCmd(CC.ApiGetChat(type, id))
|
val r = sendCmd(CC.ApiGetChat(type, id))
|
||||||
if (r is CR.ApiChat ) return r.chat
|
if (r is CR.ApiChat ) return r.chat
|
||||||
Log.d("SIMPLEX", "apiGetChat bad response: ${r.responseType} ${r.details}")
|
Log.e(TAG, "apiGetChat bad response: ${r.responseType} ${r.details}")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiSendMessage(type: ChatType, id: Long, mc: MsgContent): AChatItem? {
|
suspend fun apiSendMessage(type: ChatType, id: Long, mc: MsgContent): AChatItem? {
|
||||||
val r = sendCmd(CC.ApiSendMessage(type, id, mc))
|
val r = sendCmd(CC.ApiSendMessage(type, id, mc))
|
||||||
if (r is CR.NewChatItem ) return r.chatItem
|
if (r is CR.NewChatItem ) return r.chatItem
|
||||||
Log.d("SIMPLEX", "apiSendMessage bad response: ${r.responseType} ${r.details}")
|
Log.e(TAG, "apiSendMessage bad response: ${r.responseType} ${r.details}")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getUserSMPServers(): List<String>? {
|
||||||
|
val r = sendCmd(CC.GetUserSMPServers())
|
||||||
|
if (r is CR.UserSMPServers) return r.smpServers
|
||||||
|
Log.e(TAG, "getUserSMPServers bad response: ${r.responseType} ${r.details}")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setUserSMPServers(smpServers: List<String>): Boolean {
|
||||||
|
val r = sendCmd(CC.SetUserSMPServers(smpServers))
|
||||||
|
return when (r) {
|
||||||
|
is CR.CmdOk -> true
|
||||||
|
else -> {
|
||||||
|
Log.e(TAG, "setUserSMPServers bad response: ${r.responseType} ${r.details}")
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
"Error saving SMP servers",
|
||||||
|
"Make sure SMP server addresses are in correct format, line separated and are not duplicated"
|
||||||
|
)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun apiAddContact(): String? {
|
suspend fun apiAddContact(): String? {
|
||||||
val r = sendCmd(CC.AddContact())
|
val r = sendCmd(CC.AddContact())
|
||||||
if (r is CR.Invitation) return r.connReqInvitation
|
if (r is CR.Invitation) return r.connReqInvitation
|
||||||
Log.d("SIMPLEX", "apiAddContact bad response: ${r.responseType} ${r.details}")
|
Log.e(TAG, "apiAddContact bad response: ${r.responseType} ${r.details}")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,14 +163,14 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert
|
|||||||
when {
|
when {
|
||||||
r is CR.SentConfirmation || r is CR.SentInvitation -> return true
|
r is CR.SentConfirmation || r is CR.SentInvitation -> return true
|
||||||
r is CR.ContactAlreadyExists -> {
|
r is CR.ContactAlreadyExists -> {
|
||||||
alertManager.showAlertMsg("Contact already exists",
|
AlertManager.shared.showAlertMsg("Contact already exists",
|
||||||
"You are already connected to ${r.contact.displayName} via this link"
|
"You are already connected to ${r.contact.displayName} via this link"
|
||||||
)
|
)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat
|
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat
|
||||||
&& r.chatError.errorType is ChatErrorType.InvalidConnReq -> {
|
&& r.chatError.errorType is ChatErrorType.InvalidConnReq -> {
|
||||||
alertManager.showAlertMsg("Invalid connection link",
|
AlertManager.shared.showAlertMsg("Invalid connection link",
|
||||||
"Please check that you used the correct link or ask your contact to send you another one."
|
"Please check that you used the correct link or ask your contact to send you another one."
|
||||||
)
|
)
|
||||||
return false
|
return false
|
||||||
@@ -135,20 +184,19 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert
|
|||||||
|
|
||||||
suspend fun apiDeleteChat(type: ChatType, id: Long): Boolean {
|
suspend fun apiDeleteChat(type: ChatType, id: Long): Boolean {
|
||||||
val r = sendCmd(CC.ApiDeleteChat(type, id))
|
val r = sendCmd(CC.ApiDeleteChat(type, id))
|
||||||
when {
|
when (r) {
|
||||||
r is CR.ContactDeleted -> return true // TODO groups
|
is CR.ContactDeleted -> return true // TODO groups
|
||||||
r is CR.ChatCmdError -> {
|
is CR.ChatCmdError -> {
|
||||||
val e = r.chatError
|
val e = r.chatError
|
||||||
if (e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.ContactGroups) {
|
if (e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.ContactGroups) {
|
||||||
alertManager.showAlertMsg(
|
AlertManager.shared.showAlertMsg(
|
||||||
"Can't delete contact!",
|
"Can't delete contact!",
|
||||||
"Contact ${e.errorType.contact.displayName} cannot be deleted, it is a member of the group(s) ${e.errorType.groupNames}"
|
"Contact ${e.errorType.contact.displayName} cannot be deleted, it is a member of the group(s) ${e.errorType.groupNames}"
|
||||||
)
|
)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else -> apiErrorAlert("apiDeleteChat", "Error deleting ${type.chatTypeName}", r)
|
||||||
}
|
}
|
||||||
apiErrorAlert("apiDeleteChat", "Error deleting ${type.chatTypeName}", r)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,21 +204,21 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert
|
|||||||
val r = sendCmd(CC.UpdateProfile(profile))
|
val r = sendCmd(CC.UpdateProfile(profile))
|
||||||
if (r is CR.UserProfileNoChange) return profile
|
if (r is CR.UserProfileNoChange) return profile
|
||||||
if (r is CR.UserProfileUpdated) return r.toProfile
|
if (r is CR.UserProfileUpdated) return r.toProfile
|
||||||
Log.d("SIMPLEX", "apiUpdateProfile bad response: ${r.responseType} ${r.details}")
|
Log.e(TAG, "apiUpdateProfile bad response: ${r.responseType} ${r.details}")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiCreateUserAddress(): String? {
|
suspend fun apiCreateUserAddress(): String? {
|
||||||
val r = sendCmd(CC.CreateMyAddress())
|
val r = sendCmd(CC.CreateMyAddress())
|
||||||
if (r is CR.UserContactLinkCreated) return r.connReqContact
|
if (r is CR.UserContactLinkCreated) return r.connReqContact
|
||||||
Log.d("SIMPLEX", "apiCreateUserAddress bad response: ${r.responseType} ${r.details}")
|
Log.e(TAG, "apiCreateUserAddress bad response: ${r.responseType} ${r.details}")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiDeleteUserAddress(): Boolean {
|
suspend fun apiDeleteUserAddress(): Boolean {
|
||||||
val r = sendCmd(CC.DeleteMyAddress())
|
val r = sendCmd(CC.DeleteMyAddress())
|
||||||
if (r is CR.UserContactLinkDeleted) return true
|
if (r is CR.UserContactLinkDeleted) return true
|
||||||
Log.d("SIMPLEX", "apiDeleteUserAddress bad response: ${r.responseType} ${r.details}")
|
Log.e(TAG, "apiDeleteUserAddress bad response: ${r.responseType} ${r.details}")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,35 +229,35 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert
|
|||||||
&& r.chatError.storeError is StoreError.UserContactLinkNotFound) {
|
&& r.chatError.storeError is StoreError.UserContactLinkNotFound) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
Log.d("SIMPLEX", "apiGetUserAddress bad response: ${r.responseType} ${r.details}")
|
Log.e(TAG, "apiGetUserAddress bad response: ${r.responseType} ${r.details}")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiAcceptContactRequest(contactReqId: Long): Contact? {
|
suspend fun apiAcceptContactRequest(contactReqId: Long): Contact? {
|
||||||
val r = sendCmd(CC.ApiAcceptContact(contactReqId))
|
val r = sendCmd(CC.ApiAcceptContact(contactReqId))
|
||||||
if (r is CR.AcceptingContactRequest) return r.contact
|
if (r is CR.AcceptingContactRequest) return r.contact
|
||||||
Log.d("SIMPLEX", "apiAcceptContactRequest bad response: ${r.responseType} ${r.details}")
|
Log.e(TAG, "apiAcceptContactRequest bad response: ${r.responseType} ${r.details}")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiRejectContactRequest(contactReqId: Long): Boolean {
|
suspend fun apiRejectContactRequest(contactReqId: Long): Boolean {
|
||||||
val r = sendCmd(CC.ApiRejectContact(contactReqId))
|
val r = sendCmd(CC.ApiRejectContact(contactReqId))
|
||||||
if (r is CR.ContactRequestRejected) return true
|
if (r is CR.ContactRequestRejected) return true
|
||||||
Log.d("SIMPLEX", "apiRejectContactRequest bad response: ${r.responseType} ${r.details}")
|
Log.e(TAG, "apiRejectContactRequest bad response: ${r.responseType} ${r.details}")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiChatRead(type: ChatType, id: Long, range: CC.ItemRange): Boolean {
|
suspend fun apiChatRead(type: ChatType, id: Long, range: CC.ItemRange): Boolean {
|
||||||
val r = sendCmd(CC.ApiChatRead(type, id, range))
|
val r = sendCmd(CC.ApiChatRead(type, id, range))
|
||||||
if (r is CR.CmdOk) return true
|
if (r is CR.CmdOk) return true
|
||||||
Log.d("SIMPLEX", "apiChatRead bad response: ${r.responseType} ${r.details}")
|
Log.e(TAG, "apiChatRead bad response: ${r.responseType} ${r.details}")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun apiErrorAlert(method: String, title: String, r: CR) {
|
fun apiErrorAlert(method: String, title: String, r: CR) {
|
||||||
val errMsg = "${r.responseType}: ${r.details}"
|
val errMsg = "${r.responseType}: ${r.details}"
|
||||||
Log.e("SIMPLEX", "$method bad response: $errMsg")
|
Log.e(TAG, "$method bad response: $errMsg")
|
||||||
alertManager.showAlertMsg(title, errMsg)
|
AlertManager.shared.showAlertMsg(title, errMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun processReceivedMsg(r: CR) {
|
fun processReceivedMsg(r: CR) {
|
||||||
@@ -220,62 +268,70 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert
|
|||||||
chatModel.updateNetworkStatus(r.contact, Chat.NetworkStatus.Connected())
|
chatModel.updateNetworkStatus(r.contact, Chat.NetworkStatus.Connected())
|
||||||
// NtfManager.shared.notifyContactConnected(contact)
|
// NtfManager.shared.notifyContactConnected(contact)
|
||||||
}
|
}
|
||||||
// is CR.ReceivedContactRequest -> return
|
is CR.ReceivedContactRequest -> {
|
||||||
|
val contactRequest = r.contactRequest
|
||||||
|
val cInfo = ChatInfo.ContactRequest(contactRequest)
|
||||||
|
chatModel.addChat(Chat(chatInfo = cInfo, chatItems = listOf()))
|
||||||
|
// NtfManager.shared.notifyContactRequest(contactRequest)
|
||||||
|
}
|
||||||
is CR.ContactUpdated -> {
|
is CR.ContactUpdated -> {
|
||||||
val cInfo = ChatInfo.Direct(r.toContact)
|
val cInfo = ChatInfo.Direct(r.toContact)
|
||||||
if (chatModel.hasChat(r.toContact.id)) {
|
if (chatModel.hasChat(r.toContact.id)) {
|
||||||
chatModel.updateChatInfo(cInfo)
|
chatModel.updateChatInfo(cInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is CR.ContactSubscribed -> {
|
is CR.ContactSubscribed -> processContactSubscribed(r.contact)
|
||||||
chatModel.updateContact(r.contact)
|
|
||||||
chatModel.updateNetworkStatus(r.contact, Chat.NetworkStatus.Connected())
|
|
||||||
}
|
|
||||||
is CR.ContactDisconnected -> {
|
is CR.ContactDisconnected -> {
|
||||||
chatModel.updateContact(r.contact)
|
chatModel.updateContact(r.contact)
|
||||||
chatModel.updateNetworkStatus(r.contact, Chat.NetworkStatus.Disconnected())
|
chatModel.updateNetworkStatus(r.contact, Chat.NetworkStatus.Disconnected())
|
||||||
}
|
}
|
||||||
is CR.ContactSubError -> {
|
is CR.ContactSubError -> processContactSubError(r.contact, r.chatError)
|
||||||
chatModel.updateContact(r.contact)
|
is CR.ContactSubSummary -> {
|
||||||
val e = r.chatError
|
for (sub in r.contactSubscriptions) {
|
||||||
val err: String =
|
val err = sub.contactError
|
||||||
if (e is ChatError.ChatErrorAgent) {
|
if (err == null) processContactSubscribed(sub.contact)
|
||||||
val a = e.agentError
|
else processContactSubError(sub.contact, sub.contactError)
|
||||||
when {
|
}
|
||||||
a is AgentErrorType.BROKER && a.brokerErr is BrokerErrorType.NETWORK -> "network"
|
|
||||||
a is AgentErrorType.SMP && a.smpErr is SMPErrorType.AUTH -> "contact deleted"
|
|
||||||
else -> e.string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else e.string
|
|
||||||
chatModel.updateNetworkStatus(r.contact, Chat.NetworkStatus.Error(err))
|
|
||||||
}
|
}
|
||||||
is CR.NewChatItem -> {
|
is CR.NewChatItem -> {
|
||||||
val cInfo = r.chatItem.chatInfo
|
val cInfo = r.chatItem.chatInfo
|
||||||
val cItem = r.chatItem.chatItem
|
val cItem = r.chatItem.chatItem
|
||||||
chatModel.addChatItem(cInfo, cItem)
|
chatModel.addChatItem(cInfo, cItem)
|
||||||
// NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
if (!isAppOnForeground(appContext) || chatModel.chatId.value != cInfo.id) {
|
||||||
|
ntfManager.notifyMessageReceived(cInfo, cItem)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// switch res {
|
|
||||||
// case let .receivedContactRequest(contactRequest):
|
|
||||||
// chatModel.addChat(Chat(
|
|
||||||
// chatInfo: ChatInfo.contactRequest(contactRequest: contactRequest),
|
|
||||||
// chatItems: []
|
|
||||||
// ))
|
|
||||||
// NtfManager.shared.notifyContactRequest(contactRequest)
|
|
||||||
//
|
|
||||||
// case let .chatItemUpdated(aChatItem):
|
// case let .chatItemUpdated(aChatItem):
|
||||||
// let cInfo = aChatItem.chatInfo
|
// let cInfo = aChatItem.chatInfo
|
||||||
// let cItem = aChatItem.chatItem
|
// let cItem = aChatItem.chatItem
|
||||||
// if chatModel.upsertChatItem(cInfo, cItem) {
|
// if chatModel.upsertChatItem(cInfo, cItem) {
|
||||||
// NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
// NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
||||||
// }
|
// }
|
||||||
// default:
|
else ->
|
||||||
// logger.debug("unsupported event: \(res.responseType)")
|
Log.d(TAG , "unsupported event: ${r.responseType}")
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun processContactSubscribed(contact: Contact) {
|
||||||
|
chatModel.updateContact(contact)
|
||||||
|
chatModel.updateNetworkStatus(contact, Chat.NetworkStatus.Connected())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun processContactSubError(contact: Contact, chatError: ChatError) {
|
||||||
|
chatModel.updateContact(contact)
|
||||||
|
val e = chatError
|
||||||
|
val err: String =
|
||||||
|
if (e is ChatError.ChatErrorAgent) {
|
||||||
|
val a = e.agentError
|
||||||
|
when {
|
||||||
|
a is AgentErrorType.BROKER && a.brokerErr is BrokerErrorType.NETWORK -> "network"
|
||||||
|
a is AgentErrorType.SMP && a.smpErr is SMPErrorType.AUTH -> "contact deleted"
|
||||||
|
else -> e.string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else e.string
|
||||||
|
chatModel.updateNetworkStatus(contact, Chat.NetworkStatus.Error(err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChatCommand
|
// ChatCommand
|
||||||
@@ -287,6 +343,8 @@ sealed class CC {
|
|||||||
class ApiGetChats: CC()
|
class ApiGetChats: CC()
|
||||||
class ApiGetChat(val type: ChatType, val id: Long): CC()
|
class ApiGetChat(val type: ChatType, val id: Long): CC()
|
||||||
class ApiSendMessage(val type: ChatType, val id: Long, val mc: MsgContent): CC()
|
class ApiSendMessage(val type: ChatType, val id: Long, val mc: MsgContent): CC()
|
||||||
|
class GetUserSMPServers(): CC()
|
||||||
|
class SetUserSMPServers(val smpServers: List<String>): CC()
|
||||||
class AddContact: CC()
|
class AddContact: CC()
|
||||||
class Connect(val connReq: String): CC()
|
class Connect(val connReq: String): CC()
|
||||||
class ApiDeleteChat(val type: ChatType, val id: Long): CC()
|
class ApiDeleteChat(val type: ChatType, val id: Long): CC()
|
||||||
@@ -306,6 +364,8 @@ sealed class CC {
|
|||||||
is ApiGetChats -> "/_get chats"
|
is ApiGetChats -> "/_get chats"
|
||||||
is ApiGetChat -> "/_get chat ${chatRef(type, id)} count=100"
|
is ApiGetChat -> "/_get chat ${chatRef(type, id)} count=100"
|
||||||
is ApiSendMessage -> "/_send ${chatRef(type, id)} ${mc.cmdString}"
|
is ApiSendMessage -> "/_send ${chatRef(type, id)} ${mc.cmdString}"
|
||||||
|
is GetUserSMPServers -> "/smp_servers"
|
||||||
|
is SetUserSMPServers -> "/smp_servers ${smpServersStr(smpServers)}"
|
||||||
is AddContact -> "/connect"
|
is AddContact -> "/connect"
|
||||||
is Connect -> "/connect $connReq"
|
is Connect -> "/connect $connReq"
|
||||||
is ApiDeleteChat -> "/_delete ${chatRef(type, id)}"
|
is ApiDeleteChat -> "/_delete ${chatRef(type, id)}"
|
||||||
@@ -326,6 +386,8 @@ sealed class CC {
|
|||||||
is ApiGetChats -> "apiGetChats"
|
is ApiGetChats -> "apiGetChats"
|
||||||
is ApiGetChat -> "apiGetChat"
|
is ApiGetChat -> "apiGetChat"
|
||||||
is ApiSendMessage -> "apiSendMessage"
|
is ApiSendMessage -> "apiSendMessage"
|
||||||
|
is GetUserSMPServers -> "getUserSMPServers"
|
||||||
|
is SetUserSMPServers -> "setUserSMPServers"
|
||||||
is AddContact -> "addContact"
|
is AddContact -> "addContact"
|
||||||
is Connect -> "connect"
|
is Connect -> "connect"
|
||||||
is ApiDeleteChat -> "apiDeleteChat"
|
is ApiDeleteChat -> "apiDeleteChat"
|
||||||
@@ -342,6 +404,8 @@ sealed class CC {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}"
|
fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}"
|
||||||
|
|
||||||
|
fun smpServersStr(smpServers: List<String>) = if (smpServers.isEmpty()) "default" else smpServers.joinToString(separator = ",")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,8 +440,10 @@ class APIResponse(val resp: CR, val corr: String? = null) {
|
|||||||
sealed class CR {
|
sealed class CR {
|
||||||
@Serializable @SerialName("activeUser") class ActiveUser(val user: User): CR()
|
@Serializable @SerialName("activeUser") class ActiveUser(val user: User): CR()
|
||||||
@Serializable @SerialName("chatStarted") class ChatStarted: CR()
|
@Serializable @SerialName("chatStarted") class ChatStarted: CR()
|
||||||
|
@Serializable @SerialName("chatRunning") class ChatRunning: CR()
|
||||||
@Serializable @SerialName("apiChats") class ApiChats(val chats: List<Chat>): CR()
|
@Serializable @SerialName("apiChats") class ApiChats(val chats: List<Chat>): CR()
|
||||||
@Serializable @SerialName("apiChat") class ApiChat(val chat: Chat): CR()
|
@Serializable @SerialName("apiChat") class ApiChat(val chat: Chat): CR()
|
||||||
|
@Serializable @SerialName("userSMPServers") class UserSMPServers(val smpServers: List<String>): CR()
|
||||||
@Serializable @SerialName("invitation") class Invitation(val connReqInvitation: String): CR()
|
@Serializable @SerialName("invitation") class Invitation(val connReqInvitation: String): CR()
|
||||||
@Serializable @SerialName("sentConfirmation") class SentConfirmation: CR()
|
@Serializable @SerialName("sentConfirmation") class SentConfirmation: CR()
|
||||||
@Serializable @SerialName("sentInvitation") class SentInvitation: CR()
|
@Serializable @SerialName("sentInvitation") class SentInvitation: CR()
|
||||||
@@ -396,7 +462,9 @@ sealed class CR {
|
|||||||
@Serializable @SerialName("contactSubscribed") class ContactSubscribed(val contact: Contact): CR()
|
@Serializable @SerialName("contactSubscribed") class ContactSubscribed(val contact: Contact): CR()
|
||||||
@Serializable @SerialName("contactDisconnected") class ContactDisconnected(val contact: Contact): CR()
|
@Serializable @SerialName("contactDisconnected") class ContactDisconnected(val contact: Contact): CR()
|
||||||
@Serializable @SerialName("contactSubError") class ContactSubError(val contact: Contact, val chatError: ChatError): CR()
|
@Serializable @SerialName("contactSubError") class ContactSubError(val contact: Contact, val chatError: ChatError): CR()
|
||||||
|
@Serializable @SerialName("contactSubSummary") class ContactSubSummary(val contactSubscriptions: List<ContactSubStatus>): CR()
|
||||||
@Serializable @SerialName("groupSubscribed") class GroupSubscribed(val group: GroupInfo): CR()
|
@Serializable @SerialName("groupSubscribed") class GroupSubscribed(val group: GroupInfo): CR()
|
||||||
|
@Serializable @SerialName("memberSubErrors") class MemberSubErrors(val memberSubErrors: List<MemberSubError>): CR()
|
||||||
@Serializable @SerialName("groupEmpty") class GroupEmpty(val group: GroupInfo): CR()
|
@Serializable @SerialName("groupEmpty") class GroupEmpty(val group: GroupInfo): CR()
|
||||||
@Serializable @SerialName("userContactLinkSubscribed") class UserContactLinkSubscribed: CR()
|
@Serializable @SerialName("userContactLinkSubscribed") class UserContactLinkSubscribed: CR()
|
||||||
@Serializable @SerialName("newChatItem") class NewChatItem(val chatItem: AChatItem): CR()
|
@Serializable @SerialName("newChatItem") class NewChatItem(val chatItem: AChatItem): CR()
|
||||||
@@ -410,8 +478,10 @@ sealed class CR {
|
|||||||
val responseType: String get() = when(this) {
|
val responseType: String get() = when(this) {
|
||||||
is ActiveUser -> "activeUser"
|
is ActiveUser -> "activeUser"
|
||||||
is ChatStarted -> "chatStarted"
|
is ChatStarted -> "chatStarted"
|
||||||
|
is ChatRunning -> "chatRunning"
|
||||||
is ApiChats -> "apiChats"
|
is ApiChats -> "apiChats"
|
||||||
is ApiChat -> "apiChats"
|
is ApiChat -> "apiChat"
|
||||||
|
is UserSMPServers -> "userSMPServers"
|
||||||
is Invitation -> "invitation"
|
is Invitation -> "invitation"
|
||||||
is SentConfirmation -> "sentConfirmation"
|
is SentConfirmation -> "sentConfirmation"
|
||||||
is SentInvitation -> "sentInvitation"
|
is SentInvitation -> "sentInvitation"
|
||||||
@@ -430,7 +500,9 @@ sealed class CR {
|
|||||||
is ContactSubscribed -> "contactSubscribed"
|
is ContactSubscribed -> "contactSubscribed"
|
||||||
is ContactDisconnected -> "contactDisconnected"
|
is ContactDisconnected -> "contactDisconnected"
|
||||||
is ContactSubError -> "contactSubError"
|
is ContactSubError -> "contactSubError"
|
||||||
|
is ContactSubSummary -> "contactSubSummary"
|
||||||
is GroupSubscribed -> "groupSubscribed"
|
is GroupSubscribed -> "groupSubscribed"
|
||||||
|
is MemberSubErrors -> "memberSubErrors"
|
||||||
is GroupEmpty -> "groupEmpty"
|
is GroupEmpty -> "groupEmpty"
|
||||||
is UserContactLinkSubscribed -> "userContactLinkSubscribed"
|
is UserContactLinkSubscribed -> "userContactLinkSubscribed"
|
||||||
is NewChatItem -> "newChatItem"
|
is NewChatItem -> "newChatItem"
|
||||||
@@ -445,8 +517,10 @@ sealed class CR {
|
|||||||
val details: String get() = when(this) {
|
val details: String get() = when(this) {
|
||||||
is ActiveUser -> json.encodeToString(user)
|
is ActiveUser -> json.encodeToString(user)
|
||||||
is ChatStarted -> noDetails()
|
is ChatStarted -> noDetails()
|
||||||
|
is ChatRunning -> noDetails()
|
||||||
is ApiChats -> json.encodeToString(chats)
|
is ApiChats -> json.encodeToString(chats)
|
||||||
is ApiChat -> json.encodeToString(chat)
|
is ApiChat -> json.encodeToString(chat)
|
||||||
|
is UserSMPServers -> json.encodeToString(smpServers)
|
||||||
is Invitation -> connReqInvitation
|
is Invitation -> connReqInvitation
|
||||||
is SentConfirmation -> noDetails()
|
is SentConfirmation -> noDetails()
|
||||||
is SentInvitation -> noDetails()
|
is SentInvitation -> noDetails()
|
||||||
@@ -465,7 +539,9 @@ sealed class CR {
|
|||||||
is ContactSubscribed -> json.encodeToString(contact)
|
is ContactSubscribed -> json.encodeToString(contact)
|
||||||
is ContactDisconnected -> json.encodeToString(contact)
|
is ContactDisconnected -> json.encodeToString(contact)
|
||||||
is ContactSubError -> "error:\n${chatError.string}\ncontact:\n${json.encodeToString(contact)}"
|
is ContactSubError -> "error:\n${chatError.string}\ncontact:\n${json.encodeToString(contact)}"
|
||||||
|
is ContactSubSummary -> json.encodeToString(contactSubscriptions)
|
||||||
is GroupSubscribed -> json.encodeToString(group)
|
is GroupSubscribed -> json.encodeToString(group)
|
||||||
|
is MemberSubErrors -> json.encodeToString(memberSubErrors)
|
||||||
is GroupEmpty -> json.encodeToString(group)
|
is GroupEmpty -> json.encodeToString(group)
|
||||||
is UserContactLinkSubscribed -> noDetails()
|
is UserContactLinkSubscribed -> noDetails()
|
||||||
is NewChatItem -> json.encodeToString(chatItem)
|
is NewChatItem -> json.encodeToString(chatItem)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ val Teal200 = Color(0xFF03DAC5)
|
|||||||
val Gray = Color(0x22222222)
|
val Gray = Color(0x22222222)
|
||||||
val SimplexBlue = Color(0, 136, 255, 255)
|
val SimplexBlue = Color(0, 136, 255, 255)
|
||||||
val SimplexGreen = Color(98, 196, 103, 255)
|
val SimplexGreen = Color(98, 196, 103, 255)
|
||||||
|
val SecretColor = Color(0x40808080)
|
||||||
val LightGray = Color(241, 242, 246, 255)
|
val LightGray = Color(241, 242, 246, 255)
|
||||||
val DarkGray = Color(43, 44, 46, 255)
|
val DarkGray = Color(43, 44, 46, 255)
|
||||||
val HighOrLowlight = Color(134, 135, 139, 255)
|
val HighOrLowlight = Color(134, 135, 139, 255)
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ val Typography = Typography(
|
|||||||
h3 = TextStyle(
|
h3 = TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 20.sp
|
fontSize = 19.sp
|
||||||
),
|
),
|
||||||
body1 = TextStyle(
|
body1 = TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 18.sp
|
fontSize = 17.sp
|
||||||
),
|
),
|
||||||
body2 = TextStyle(
|
body2 = TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
package chat.simplex.app.views
|
package chat.simplex.app.views
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import chat.simplex.app.R
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SplashView() {
|
fun SplashView() {
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Surface(
|
||||||
Image(
|
Modifier
|
||||||
painter = painterResource(R.drawable.logo),
|
.background(MaterialTheme.colors.background)
|
||||||
contentDescription = "Simplex Icon",
|
.fillMaxSize()
|
||||||
modifier = Modifier
|
) {
|
||||||
.height(230.dp)
|
// Image(
|
||||||
.align(Alignment.Center)
|
// painter = painterResource(R.drawable.logo),
|
||||||
)
|
// contentDescription = "Simplex Icon",
|
||||||
|
// modifier = Modifier
|
||||||
|
// .height(230.dp)
|
||||||
|
// .align(Alignment.Center)
|
||||||
|
// )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package chat.simplex.app.views
|
package chat.simplex.app.views
|
||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.*
|
import androidx.compose.foundation.lazy.*
|
||||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
@@ -11,24 +13,24 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
|
||||||
import chat.simplex.app.model.*
|
import chat.simplex.app.model.*
|
||||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||||
import chat.simplex.app.views.chat.SendMsgView
|
import chat.simplex.app.views.chat.SendMsgView
|
||||||
import chat.simplex.app.views.helpers.CloseSheetBar
|
import chat.simplex.app.views.helpers.CloseSheetBar
|
||||||
import chat.simplex.app.views.helpers.withApi
|
import chat.simplex.app.views.helpers.withApi
|
||||||
|
import chat.simplex.app.views.newchat.ModalManager
|
||||||
import com.google.accompanist.insets.ProvideWindowInsets
|
import com.google.accompanist.insets.ProvideWindowInsets
|
||||||
import com.google.accompanist.insets.navigationBarsWithImePadding
|
import com.google.accompanist.insets.navigationBarsWithImePadding
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TerminalView(chatModel: ChatModel, nav: NavController) {
|
fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
|
||||||
TerminalLayout(chatModel.terminalItems, nav::popBackStack, nav::navigate) { cmd ->
|
BackHandler(onBack = close)
|
||||||
|
TerminalLayout(chatModel.terminalItems, close) { cmd ->
|
||||||
withApi {
|
withApi {
|
||||||
// show "in progress"
|
// show "in progress"
|
||||||
chatModel.controller.sendCmd(CC.Console(cmd))
|
chatModel.controller.sendCmd(CC.Console(cmd))
|
||||||
@@ -38,36 +40,45 @@ fun TerminalView(chatModel: ChatModel, nav: NavController) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TerminalLayout(terminalItems: List<TerminalItem> , close: () -> Unit, navigate: (String) -> Unit,
|
fun TerminalLayout(terminalItems: List<TerminalItem> , close: () -> Unit, sendCommand: (String) -> Unit) {
|
||||||
sendCommand: (String) -> Unit) {
|
|
||||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { CloseSheetBar(close) },
|
topBar = { CloseSheetBar(close) },
|
||||||
bottomBar = { SendMsgView(sendCommand) },
|
bottomBar = { SendMsgView(sendCommand) },
|
||||||
modifier = Modifier.navigationBarsWithImePadding()
|
modifier = Modifier.navigationBarsWithImePadding()
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
Box(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(contentPadding)
|
.padding(contentPadding)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(MaterialTheme.colors.background)
|
.background(MaterialTheme.colors.background)
|
||||||
) {
|
) {
|
||||||
TerminalLog(terminalItems, navigate)
|
TerminalLog(terminalItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TerminalLog(terminalItems: List<TerminalItem>, navigate: (String) -> Unit) {
|
fun TerminalLog(terminalItems: List<TerminalItem>) {
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
LazyColumn(state = listState) {
|
LazyColumn(state = listState) {
|
||||||
items(terminalItems) { item ->
|
items(terminalItems) { 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),
|
style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 18.sp, color = MaterialTheme.colors.primary),
|
||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
|
maxLines = 1,
|
||||||
.clickable { navigate("details/${item.id}") })
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
.clickable {
|
||||||
|
ModalManager.shared.showModal {
|
||||||
|
SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||||
|
Text(item.details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val len = terminalItems.count()
|
val len = terminalItems.count()
|
||||||
if (len > 1) {
|
if (len > 1) {
|
||||||
@@ -78,20 +89,6 @@ fun TerminalLog(terminalItems: List<TerminalItem>, navigate: (String) -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DetailView(identifier: Long, terminalItems: List<TerminalItem>, navController: NavController){
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.verticalScroll(rememberScrollState())
|
|
||||||
) {
|
|
||||||
Button(onClick = { navController.popBackStack() }) {
|
|
||||||
Text("Back")
|
|
||||||
}
|
|
||||||
SelectionContainer {
|
|
||||||
Text((terminalItems.firstOrNull { it.id == identifier })?.details ?: "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Preview(
|
@Preview(
|
||||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||||
@@ -104,7 +101,6 @@ fun PreviewTerminalLayout() {
|
|||||||
TerminalLayout(
|
TerminalLayout(
|
||||||
terminalItems = TerminalItem.sampleData,
|
terminalItems = TerminalItem.sampleData,
|
||||||
close = {},
|
close = {},
|
||||||
navigate = {},
|
|
||||||
sendCommand = {}
|
sendCommand = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,58 +2,158 @@ package chat.simplex.app.views
|
|||||||
|
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
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.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import chat.simplex.app.R
|
import chat.simplex.app.R
|
||||||
import chat.simplex.app.model.ChatModel
|
import chat.simplex.app.model.ChatModel
|
||||||
import chat.simplex.app.model.Profile
|
import chat.simplex.app.model.Profile
|
||||||
import chat.simplex.app.views.helpers.withApi
|
import chat.simplex.app.views.helpers.withApi
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import com.google.accompanist.insets.ProvideWindowInsets
|
||||||
|
import com.google.accompanist.insets.navigationBarsWithImePadding
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WelcomeView(chatModel: ChatModel, routeHome: () -> Unit) {
|
fun WelcomeView(chatModel: ChatModel) {
|
||||||
Column(
|
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||||
modifier = Modifier.verticalScroll(rememberScrollState())
|
Box(
|
||||||
) {
|
modifier = Modifier
|
||||||
Image(
|
.fillMaxSize()
|
||||||
painter=painterResource(R.drawable.logo), contentDescription = "Simplex Logo",
|
.background(color = MaterialTheme.colors.background)
|
||||||
)
|
) {
|
||||||
Text("You control your chat!")
|
Column(
|
||||||
Text("The messaging and application platform protecting your privacy and security.")
|
verticalArrangement = Arrangement.SpaceBetween,
|
||||||
Spacer(Modifier.height(8.dp))
|
modifier = Modifier
|
||||||
Text("We don't store any of your contacts or messages (once delivered) on the servers.")
|
.verticalScroll(rememberScrollState())
|
||||||
Spacer(Modifier.height(24.dp))
|
.fillMaxSize()
|
||||||
CreateProfilePanel(chatModel, routeHome)
|
.background(color = MaterialTheme.colors.background)
|
||||||
|
.padding(12.dp)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.logo),
|
||||||
|
contentDescription = "Simplex Logo",
|
||||||
|
modifier = Modifier.padding(vertical = 15.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"You control your chat!",
|
||||||
|
style = MaterialTheme.typography.h4,
|
||||||
|
color = MaterialTheme.colors.onBackground
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"The messaging and application platform protecting your privacy and security.",
|
||||||
|
style = MaterialTheme.typography.body1,
|
||||||
|
color = MaterialTheme.colors.onBackground
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
"We don't store any of your contacts or messages (once delivered) on the servers.",
|
||||||
|
style = MaterialTheme.typography.body1,
|
||||||
|
color = MaterialTheme.colors.onBackground
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
CreateProfilePanel(chatModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
|
||||||
|
fun isValidDisplayName(name: String) : Boolean {
|
||||||
|
return (name.firstOrNull { it.isWhitespace() }) == null
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CreateProfilePanel(chatModel: ChatModel, routeHome: () -> Unit) {
|
fun CreateProfilePanel(chatModel: ChatModel) {
|
||||||
var displayName by remember { mutableStateOf("") }
|
var displayName by remember { mutableStateOf("") }
|
||||||
var fullName by remember { mutableStateOf("") }
|
var fullName by remember { mutableStateOf("") }
|
||||||
|
|
||||||
Column {
|
Column(
|
||||||
Text("Create profile")
|
modifier=Modifier.fillMaxSize()
|
||||||
Text("Your profile is stored on your device and shared only with your contacts.")
|
) {
|
||||||
Text("Display Name")
|
Text(
|
||||||
TextField(value = displayName, onValueChange = { value -> displayName = value })
|
"Create profile",
|
||||||
Text("Full Name (Optional)")
|
style = MaterialTheme.typography.h4,
|
||||||
TextField(value = fullName, onValueChange = { fullName = it })
|
color = MaterialTheme.colors.onBackground,
|
||||||
Button(onClick={
|
modifier = Modifier.padding(vertical = 5.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Your profile is stored on your device and shared only with your contacts.",
|
||||||
|
style = MaterialTheme.typography.body1,
|
||||||
|
color = MaterialTheme.colors.onBackground
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
Text(
|
||||||
|
"Display Name",
|
||||||
|
style = MaterialTheme.typography.h6,
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
modifier = Modifier.padding(bottom = 3.dp)
|
||||||
|
)
|
||||||
|
BasicTextField(
|
||||||
|
value = displayName,
|
||||||
|
onValueChange = { displayName = it },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(MaterialTheme.colors.secondary)
|
||||||
|
.height(40.dp)
|
||||||
|
.clip(RoundedCornerShape(5.dp))
|
||||||
|
.padding(8.dp)
|
||||||
|
.navigationBarsWithImePadding(),
|
||||||
|
textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground),
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
autoCorrect = false
|
||||||
|
),
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
val errorText = if(!isValidDisplayName(displayName)) "Display name cannot contain whitespace." else ""
|
||||||
|
|
||||||
|
Text(
|
||||||
|
errorText,
|
||||||
|
fontSize = 15.sp,
|
||||||
|
color = MaterialTheme.colors.error
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(3.dp))
|
||||||
|
Text(
|
||||||
|
"Full Name (Optional)",
|
||||||
|
style = MaterialTheme.typography.h6,
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
modifier = Modifier.padding(bottom = 5.dp)
|
||||||
|
)
|
||||||
|
BasicTextField(
|
||||||
|
value = fullName,
|
||||||
|
onValueChange = { fullName = it },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(MaterialTheme.colors.secondary)
|
||||||
|
.height(40.dp)
|
||||||
|
.clip(RoundedCornerShape(3.dp))
|
||||||
|
.padding(8.dp)
|
||||||
|
.navigationBarsWithImePadding(),
|
||||||
|
textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground),
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
autoCorrect = false
|
||||||
|
),
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(20.dp))
|
||||||
|
Button(onClick = {
|
||||||
withApi {
|
withApi {
|
||||||
val user = chatModel.controller.apiCreateActiveUser(
|
val user = chatModel.controller.apiCreateActiveUser(
|
||||||
Profile(displayName, fullName)
|
Profile(displayName, fullName)
|
||||||
)
|
)
|
||||||
chatModel.controller.startChat(user)
|
chatModel.controller.startChat(user)
|
||||||
routeHome()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled = displayName.isNotEmpty()
|
enabled = (displayName.isNotEmpty() && isValidDisplayName(displayName))
|
||||||
) { Text("Create")}
|
) { Text("Create") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package chat.simplex.app.views.chat
|
package chat.simplex.app.views.chat
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
@@ -14,22 +15,19 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
|
||||||
import chat.simplex.app.Pages
|
|
||||||
import chat.simplex.app.model.*
|
import chat.simplex.app.model.*
|
||||||
import chat.simplex.app.ui.theme.*
|
import chat.simplex.app.ui.theme.*
|
||||||
import chat.simplex.app.views.helpers.*
|
import chat.simplex.app.views.helpers.*
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatInfoView(chatModel: ChatModel, nav: NavController) {
|
fun ChatInfoView(chatModel: ChatModel, close: () -> Unit) {
|
||||||
|
BackHandler(onBack = close)
|
||||||
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
|
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
|
||||||
if (chat != null) {
|
if (chat != null) {
|
||||||
ChatInfoLayout(chat,
|
ChatInfoLayout(chat,
|
||||||
close = { nav.popBackStack() },
|
close = close,
|
||||||
deleteContact = {
|
deleteContact = {
|
||||||
chatModel.alertManager.showAlertMsg(
|
AlertManager.shared.showAlertMsg(
|
||||||
title = "Delete contact?",
|
title = "Delete contact?",
|
||||||
text = "Contact and all messages will be deleted - this cannot be undone!",
|
text = "Contact and all messages will be deleted - this cannot be undone!",
|
||||||
confirmText = "Delete",
|
confirmText = "Delete",
|
||||||
@@ -39,7 +37,8 @@ fun ChatInfoView(chatModel: ChatModel, nav: NavController) {
|
|||||||
val r = chatModel.controller.apiDeleteChat(cInfo.chatType, cInfo.apiId)
|
val r = chatModel.controller.apiDeleteChat(cInfo.chatType, cInfo.apiId)
|
||||||
if (r) {
|
if (r) {
|
||||||
chatModel.removeChat(cInfo.id)
|
chatModel.removeChat(cInfo.id)
|
||||||
nav.navigate(Pages.ChatList.route)
|
chatModel.chatId.value = null
|
||||||
|
close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,7 +93,7 @@ fun ChatInfoLayout(chat: Chat, close: () -> Unit, deleteContact: () -> Unit) {
|
|||||||
|
|
||||||
Spacer(Modifier.weight(1F))
|
Spacer(Modifier.weight(1F))
|
||||||
|
|
||||||
Box(Modifier.padding(24.dp)) {
|
Box(Modifier.padding(48.dp)) {
|
||||||
SimpleButton(
|
SimpleButton(
|
||||||
"Delete contact", icon = Icons.Outlined.Delete,
|
"Delete contact", icon = Icons.Outlined.Delete,
|
||||||
color = Color.Red,
|
color = Color.Red,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package chat.simplex.app.views.chat
|
package chat.simplex.app.views.chat
|
||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@@ -9,69 +11,69 @@ import androidx.compose.material.*
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.ArrowBack
|
import androidx.compose.material.icons.outlined.ArrowBack
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.saveable.mapSaver
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
import chat.simplex.app.TAG
|
||||||
import chat.simplex.app.Pages
|
|
||||||
import chat.simplex.app.model.*
|
import chat.simplex.app.model.*
|
||||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||||
import chat.simplex.app.views.chat.item.ChatItemView
|
import chat.simplex.app.views.chat.item.ChatItemView
|
||||||
import chat.simplex.app.views.helpers.ChatInfoImage
|
import chat.simplex.app.views.helpers.*
|
||||||
import chat.simplex.app.views.helpers.withApi
|
import chat.simplex.app.views.newchat.ModalManager
|
||||||
import com.google.accompanist.insets.*
|
import com.google.accompanist.insets.ProvideWindowInsets
|
||||||
import kotlinx.coroutines.*
|
import com.google.accompanist.insets.navigationBarsWithImePadding
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@ExperimentalAnimatedInsets
|
|
||||||
@DelicateCoroutinesApi
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatView(chatModel: ChatModel, nav: NavController) {
|
fun ChatView(chatModel: ChatModel) {
|
||||||
if (chatModel.chatId.value != null && chatModel.chats.count() > 0) {
|
val chat: Chat? = chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }
|
||||||
val chat: Chat? = chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }
|
if (chat == null) {
|
||||||
if (chat != null) {
|
chatModel.chatId.value = null
|
||||||
|
} else {
|
||||||
// TODO a more advanced version would mark as read only if in view
|
BackHandler { chatModel.chatId.value = null }
|
||||||
LaunchedEffect(chat.chatItems) {
|
// TODO a more advanced version would mark as read only if in view
|
||||||
delay(1000L)
|
LaunchedEffect(chat.chatItems) {
|
||||||
if (chat.chatItems.count() > 0) {
|
Log.d(TAG, "ChatView ${chatModel.chatId.value}: LaunchedEffect")
|
||||||
chatModel.markChatItemsRead(chat.chatInfo)
|
delay(1000L)
|
||||||
withApi {
|
if (chat.chatItems.count() > 0) {
|
||||||
chatModel.controller.apiChatRead(
|
chatModel.markChatItemsRead(chat.chatInfo)
|
||||||
chat.chatInfo.chatType,
|
withApi {
|
||||||
chat.chatInfo.apiId,
|
chatModel.controller.apiChatRead(
|
||||||
CC.ItemRange(chat.chatStats.minUnreadItemId, chat.chatItems.last().id)
|
chat.chatInfo.chatType,
|
||||||
)
|
chat.chatInfo.apiId,
|
||||||
}
|
CC.ItemRange(chat.chatStats.minUnreadItemId, chat.chatItems.last().id)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChatLayout(chat, chatModel.chatItems,
|
|
||||||
back = { nav.popBackStack() },
|
|
||||||
info = { nav.navigate(Pages.ChatInfo.route) },
|
|
||||||
sendMessage = { msg ->
|
|
||||||
withApi {
|
|
||||||
// show "in progress"
|
|
||||||
val cInfo = chat.chatInfo
|
|
||||||
val newItem = chatModel.controller.apiSendMessage(
|
|
||||||
type = cInfo.chatType,
|
|
||||||
id = cInfo.apiId,
|
|
||||||
mc = MsgContent.MCText(msg)
|
|
||||||
)
|
|
||||||
// hide "in progress"
|
|
||||||
if (newItem != null) chatModel.addChatItem(cInfo, newItem.chatItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
ChatLayout(chat, chatModel.chatItems,
|
||||||
|
back = { chatModel.chatId.value = null },
|
||||||
|
info = { ModalManager.shared.showCustomModal { close -> ChatInfoView(chatModel, close) } },
|
||||||
|
sendMessage = { msg ->
|
||||||
|
withApi {
|
||||||
|
// show "in progress"
|
||||||
|
val cInfo = chat.chatInfo
|
||||||
|
val newItem = chatModel.controller.apiSendMessage(
|
||||||
|
type = cInfo.chatType,
|
||||||
|
id = cInfo.apiId,
|
||||||
|
mc = MsgContent.MCText(msg)
|
||||||
|
)
|
||||||
|
// hide "in progress"
|
||||||
|
if (newItem != null) chatModel.addChatItem(cInfo, newItem.chatItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
|
||||||
@ExperimentalAnimatedInsets
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatLayout(
|
fun ChatLayout(
|
||||||
chat: Chat, chatItems: List<ChatItem>,
|
chat: Chat, chatItems: List<ChatItem>,
|
||||||
@@ -79,19 +81,19 @@ fun ChatLayout(
|
|||||||
info: () -> Unit,
|
info: () -> Unit,
|
||||||
sendMessage: (String) -> Unit
|
sendMessage: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
Surface(
|
||||||
Scaffold(
|
Modifier
|
||||||
topBar = { ChatInfoToolbar(chat, back, info) },
|
.fillMaxWidth()
|
||||||
bottomBar = { SendMsgView(sendMessage) },
|
.background(MaterialTheme.colors.background)) {
|
||||||
modifier = Modifier.navigationBarsWithImePadding()
|
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||||
) { contentPadding ->
|
Scaffold(
|
||||||
Box(
|
topBar = { ChatInfoToolbar(chat, back, info) },
|
||||||
modifier = Modifier
|
bottomBar = { SendMsgView(sendMessage) },
|
||||||
.padding(contentPadding)
|
modifier = Modifier.navigationBarsWithImePadding()
|
||||||
.fillMaxWidth()
|
) { contentPadding ->
|
||||||
.background(MaterialTheme.colors.background)
|
Box(Modifier.padding(contentPadding)) {
|
||||||
) {
|
ChatItemsList(chatItems)
|
||||||
ChatItemsList(chatItems)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,7 +101,10 @@ fun ChatLayout(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatInfoToolbar(chat: Chat, back: () -> Unit, info: () -> Unit) {
|
fun ChatInfoToolbar(chat: Chat, back: () -> Unit, info: () -> Unit) {
|
||||||
Box(Modifier.height(60.dp).padding(horizontal = 8.dp),
|
Box(
|
||||||
|
Modifier
|
||||||
|
.height(60.dp)
|
||||||
|
.padding(horizontal = 8.dp),
|
||||||
contentAlignment = Alignment.CenterStart
|
contentAlignment = Alignment.CenterStart
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = back) {
|
IconButton(onClick = back) {
|
||||||
@@ -134,26 +139,41 @@ fun ChatInfoToolbar(chat: Chat, back: () -> Unit, info: () -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
data class CIListState(val scrolled: Boolean, val itemCount: Int, val keyboardState: KeyboardState)
|
||||||
@ExperimentalAnimatedInsets
|
|
||||||
|
val CIListStateSaver = run {
|
||||||
|
val scrolledKey = "scrolled"
|
||||||
|
val countKey = "itemCount"
|
||||||
|
val keyboardKey = "keyboardState"
|
||||||
|
mapSaver(
|
||||||
|
save = { mapOf(scrolledKey to it.scrolled, countKey to it.itemCount, keyboardKey to it.keyboardState) },
|
||||||
|
restore = { CIListState(it[scrolledKey] as Boolean, it[countKey] as Int, it[keyboardKey] as KeyboardState) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatItemsList(chatItems: List<ChatItem>) {
|
fun ChatItemsList(chatItems: List<ChatItem>) {
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
val keyboardState by getKeyboardState()
|
||||||
|
val ciListState = rememberSaveable(stateSaver = CIListStateSaver) {
|
||||||
|
mutableStateOf(CIListState(false, chatItems.count(), keyboardState))
|
||||||
|
}
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
LazyColumn(state = listState) {
|
LazyColumn(state = listState) {
|
||||||
items(chatItems) { cItem ->
|
items(chatItems) { cItem ->
|
||||||
ChatItemView(cItem)
|
ChatItemView(cItem, uriHandler)
|
||||||
}
|
}
|
||||||
val len = chatItems.count()
|
val len = chatItems.count()
|
||||||
if (len > 1) {
|
if (keyboardState != ciListState.value.keyboardState || !ciListState.value.scrolled || len != ciListState.value.itemCount) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
ciListState.value = CIListState(true, len, keyboardState)
|
||||||
listState.animateScrollToItem(len - 1)
|
listState.animateScrollToItem(len - 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExperimentalAnimatedInsets
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Preview(
|
@Preview(
|
||||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||||
|
|||||||
@@ -15,9 +15,11 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -33,6 +35,7 @@ fun SendMsgView(sendMessage: (String) -> Unit) {
|
|||||||
autoCorrect = true
|
autoCorrect = true
|
||||||
),
|
),
|
||||||
modifier = Modifier.padding(8.dp),
|
modifier = Modifier.padding(8.dp),
|
||||||
|
cursorBrush = SolidColor(HighOrLowlight),
|
||||||
decorationBox = { innerTextField ->
|
decorationBox = { innerTextField ->
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package chat.simplex.app.views.chat.item
|
package chat.simplex.app.views.chat.item
|
||||||
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import chat.simplex.app.model.CIDirection
|
import chat.simplex.app.model.CIDirection
|
||||||
import chat.simplex.app.model.ChatItem
|
import chat.simplex.app.model.ChatItem
|
||||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||||
@@ -14,7 +14,7 @@ fun CIMetaView(chatItem: ChatItem) {
|
|||||||
Text(
|
Text(
|
||||||
chatItem.timestampText,
|
chatItem.timestampText,
|
||||||
color = HighOrLowlight,
|
color = HighOrLowlight,
|
||||||
style = MaterialTheme.typography.body2
|
fontSize = 14.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.*
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.UriHandler
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import chat.simplex.app.model.CIDirection
|
import chat.simplex.app.model.CIDirection
|
||||||
@@ -12,7 +13,7 @@ import chat.simplex.app.ui.theme.SimpleXTheme
|
|||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatItemView(chatItem: ChatItem) {
|
fun ChatItemView(chatItem: ChatItem, uriHandler: UriHandler? = null) {
|
||||||
val sent = chatItem.chatDir.sent
|
val sent = chatItem.chatDir.sent
|
||||||
val alignment = if (sent) Alignment.CenterEnd else Alignment.CenterStart
|
val alignment = if (sent) Alignment.CenterEnd else Alignment.CenterStart
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@ fun ChatItemView(chatItem: ChatItem) {
|
|||||||
),
|
),
|
||||||
contentAlignment = alignment,
|
contentAlignment = alignment,
|
||||||
) {
|
) {
|
||||||
TextItemView(chatItem)
|
TextItemView(chatItem, uriHandler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
package chat.simplex.app.views.chat.item
|
package chat.simplex.app.views.chat.item
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.Surface
|
import androidx.compose.foundation.text.ClickableText
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.UriHandler
|
||||||
|
import androidx.compose.ui.text.*
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import chat.simplex.app.model.CIDirection
|
import chat.simplex.app.model.CIDirection
|
||||||
import chat.simplex.app.model.ChatItem
|
import chat.simplex.app.model.ChatItem
|
||||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||||
@@ -16,10 +24,10 @@ import kotlinx.datetime.Clock
|
|||||||
|
|
||||||
// TODO move to theme
|
// TODO move to theme
|
||||||
val SentColorLight = Color(0x1E45B8FF)
|
val SentColorLight = Color(0x1E45B8FF)
|
||||||
val ReceivedColorLight = Color(0x1EF1F0F5)
|
val ReceivedColorLight = Color(0x1EB1B0B5)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TextItemView(chatItem: ChatItem) {
|
fun TextItemView(chatItem: ChatItem, uriHandler: UriHandler? = null) {
|
||||||
val sent = chatItem.chatDir.sent
|
val sent = chatItem.chatDir.sent
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
@@ -28,14 +36,80 @@ fun TextItemView(chatItem: ChatItem) {
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier.padding(vertical = 6.dp, horizontal = 12.dp)
|
modifier = Modifier.padding(vertical = 6.dp, horizontal = 12.dp)
|
||||||
) {
|
) {
|
||||||
Column {
|
Box(contentAlignment = Alignment.BottomEnd) {
|
||||||
Text(text = chatItem.content.text)
|
MarkdownText(chatItem, uriHandler = uriHandler, groupMemberBold = true)
|
||||||
CIMetaView(chatItem)
|
CIMetaView(chatItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val reserveTimestampStyle = SpanStyle(color = Color.Transparent)
|
||||||
|
val boldFont = SpanStyle(fontWeight = FontWeight.Medium)
|
||||||
|
|
||||||
|
fun appendGroupMember(b: AnnotatedString.Builder, chatItem: ChatItem, groupMemberBold: Boolean) {
|
||||||
|
if (chatItem.chatDir is CIDirection.GroupRcv) {
|
||||||
|
val name = chatItem.chatDir.groupMember.memberProfile.displayName
|
||||||
|
if (groupMemberBold) b.withStyle(boldFont) { append(name) }
|
||||||
|
else b.append(name)
|
||||||
|
b.append(": ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MarkdownText (
|
||||||
|
chatItem: ChatItem,
|
||||||
|
style: TextStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onSurface, lineHeight = 22.sp),
|
||||||
|
maxLines: Int = Int.MAX_VALUE,
|
||||||
|
overflow: TextOverflow = TextOverflow.Clip,
|
||||||
|
uriHandler: UriHandler? = null,
|
||||||
|
groupMemberBold: Boolean = false,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
if (chatItem.formattedText == null) {
|
||||||
|
val annotatedText = buildAnnotatedString {
|
||||||
|
appendGroupMember(this, chatItem, groupMemberBold)
|
||||||
|
append(chatItem.content.text)
|
||||||
|
withStyle(reserveTimestampStyle) { append(" ${chatItem.timestampText}") }
|
||||||
|
}
|
||||||
|
SelectionContainer {
|
||||||
|
Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val annotatedText = buildAnnotatedString {
|
||||||
|
appendGroupMember(this, chatItem, groupMemberBold)
|
||||||
|
for (ft in chatItem.formattedText) {
|
||||||
|
if (ft.format == null) append(ft.text)
|
||||||
|
else {
|
||||||
|
val link = ft.link
|
||||||
|
if (link != null) {
|
||||||
|
withAnnotation(tag = "URL", annotation = link) {
|
||||||
|
withStyle(ft.format.style) { append(ft.text) }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
withStyle(ft.format.style) { append(ft.text) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withStyle(reserveTimestampStyle) { append(" ${chatItem.timestampText}") }
|
||||||
|
}
|
||||||
|
if (uriHandler != null) {
|
||||||
|
SelectionContainer {
|
||||||
|
ClickableText(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow,
|
||||||
|
onClick = { offset ->
|
||||||
|
annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset)
|
||||||
|
.firstOrNull()?.let { annotation -> uriHandler.openUri(annotation.item) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SelectionContainer {
|
||||||
|
Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun PreviewTextItemViewSnd() {
|
fun PreviewTextItemViewSnd() {
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package chat.simplex.app.views.chat
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.PersonAdd
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import androidx.compose.ui.text.*
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||||
|
import chat.simplex.app.views.usersettings.simplexTeamUri
|
||||||
|
|
||||||
|
val bold = SpanStyle(fontWeight = FontWeight.Bold)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatHelpView(addContact: (() -> Unit)? = null) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
|
||||||
|
Text("Thank you for installing SimpleX Chat!")
|
||||||
|
Text(
|
||||||
|
buildAnnotatedString {
|
||||||
|
append("You can ")
|
||||||
|
withStyle(SpanStyle(color = MaterialTheme.colors.primary)) {
|
||||||
|
append("connect to SimpleX team")
|
||||||
|
}
|
||||||
|
append(".")
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable(onClick = {
|
||||||
|
uriHandler.openUri(simplexTeamUri)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
Modifier.padding(top = 24.dp),
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"To start a new chat",
|
||||||
|
style = MaterialTheme.typography.h2
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text("Tap button")
|
||||||
|
Icon(
|
||||||
|
Icons.Outlined.PersonAdd,
|
||||||
|
"Add Contact",
|
||||||
|
modifier = if (addContact != null) Modifier.clickable(onClick = addContact) else Modifier,
|
||||||
|
)
|
||||||
|
Text("above, then:")
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
buildAnnotatedString {
|
||||||
|
withStyle(bold) { append("Add new contact") }
|
||||||
|
append(": to create your one-time QR Code for your contact.")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
buildAnnotatedString {
|
||||||
|
withStyle(bold) { append("Scan QR code") }
|
||||||
|
append(": to connect to your contact who shows QR code to you.")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
Modifier.padding(top = 24.dp),
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
Text("To connect via link", style = MaterialTheme.typography.h2)
|
||||||
|
Text("If you received SimpleX Chat invitation link you can open it in your browser:")
|
||||||
|
Text(
|
||||||
|
buildAnnotatedString {
|
||||||
|
append("\uD83D\uDCBB desktop: scan displayed QR code from the app, via ")
|
||||||
|
withStyle(bold) { append("Scan QR code") }
|
||||||
|
append(".")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
buildAnnotatedString {
|
||||||
|
append("\uD83D\uDCF1 mobile: tap ")
|
||||||
|
withStyle(bold) { append("Open in mobile app") }
|
||||||
|
append(", then tap ")
|
||||||
|
withStyle(bold) { append("Connect") }
|
||||||
|
append(" in the app.")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Preview(
|
||||||
|
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||||
|
showBackground = true,
|
||||||
|
name = "Dark Mode"
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
fun PreviewChatHelpLayout() {
|
||||||
|
SimpleXTheme {
|
||||||
|
ChatHelpView({})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
package chat.simplex.app.views.chatlist
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.Divider
|
||||||
|
import androidx.compose.material.Surface
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.toMutableStateList
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
import chat.simplex.app.model.*
|
||||||
|
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||||
|
import chat.simplex.app.views.helpers.AlertManager
|
||||||
|
import chat.simplex.app.views.helpers.withApi
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||||
|
ChatListNavLinkLayout(
|
||||||
|
chat = chat,
|
||||||
|
click = {
|
||||||
|
if (chat.chatInfo is ChatInfo.ContactRequest) {
|
||||||
|
contactRequestAlertDialog(chat.chatInfo, chatModel)
|
||||||
|
} else {
|
||||||
|
withApi { openChat(chatModel, chat.chatInfo) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun openChat(chatModel: ChatModel, cInfo: ChatInfo) {
|
||||||
|
val chat = chatModel.controller.apiGetChat(cInfo.chatType, cInfo.apiId)
|
||||||
|
if (chat != null) {
|
||||||
|
chatModel.chatItems = chat.chatItems.toMutableStateList()
|
||||||
|
chatModel.chatId.value = cInfo.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun contactRequestAlertDialog(contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) {
|
||||||
|
AlertManager.shared.showAlertDialog(
|
||||||
|
title = "Accept connection request?",
|
||||||
|
text = "If you choose to reject sender will NOT be notified",
|
||||||
|
confirmText = "Accept",
|
||||||
|
onConfirm = {
|
||||||
|
withApi {
|
||||||
|
val contact = chatModel.controller.apiAcceptContactRequest(contactRequest.apiId)
|
||||||
|
if (contact != null) {
|
||||||
|
val chat = Chat(ChatInfo.Direct(contact), listOf())
|
||||||
|
chatModel.replaceChat(contactRequest.id, chat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissText = "Reject",
|
||||||
|
onDismiss = {
|
||||||
|
withApi {
|
||||||
|
chatModel.controller.apiRejectContactRequest(contactRequest.apiId)
|
||||||
|
chatModel.removeChat(contactRequest.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatListNavLinkLayout(chat: Chat, click: () -> Unit) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = click)
|
||||||
|
.height(88.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
.padding(end = 12.dp),
|
||||||
|
verticalAlignment = Alignment.Top
|
||||||
|
) {
|
||||||
|
if (chat.chatInfo is ChatInfo.ContactRequest) {
|
||||||
|
ContactRequestView(chat)
|
||||||
|
} else {
|
||||||
|
ChatPreviewView(chat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Divider(Modifier.padding(horizontal = 8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Preview(
|
||||||
|
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||||
|
showBackground = true,
|
||||||
|
name = "Dark Mode"
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
fun PreviewChatListNavLinkDirect() {
|
||||||
|
SimpleXTheme {
|
||||||
|
ChatListNavLinkLayout(
|
||||||
|
chat = Chat(
|
||||||
|
chatInfo = ChatInfo.Direct.sampleData,
|
||||||
|
chatItems = listOf(
|
||||||
|
ChatItem.getSampleData(
|
||||||
|
1,
|
||||||
|
CIDirection.DirectSnd(),
|
||||||
|
Clock.System.now(),
|
||||||
|
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
||||||
|
)
|
||||||
|
),
|
||||||
|
chatStats = Chat.ChatStats()
|
||||||
|
),
|
||||||
|
click = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Preview(
|
||||||
|
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||||
|
showBackground = true,
|
||||||
|
name = "Dark Mode"
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
fun PreviewChatListNavLinkGroup() {
|
||||||
|
SimpleXTheme {
|
||||||
|
ChatListNavLinkLayout(
|
||||||
|
chat = Chat(
|
||||||
|
chatInfo = ChatInfo.Group.sampleData,
|
||||||
|
chatItems = listOf(
|
||||||
|
ChatItem.getSampleData(
|
||||||
|
1,
|
||||||
|
CIDirection.DirectSnd(),
|
||||||
|
Clock.System.now(),
|
||||||
|
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
||||||
|
)
|
||||||
|
),
|
||||||
|
chatStats = Chat.ChatStats()
|
||||||
|
),
|
||||||
|
click = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Preview(
|
||||||
|
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||||
|
showBackground = true,
|
||||||
|
name = "Dark Mode"
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
fun PreviewChatListNavLinkContactRequest() {
|
||||||
|
SimpleXTheme {
|
||||||
|
ChatListNavLinkLayout(
|
||||||
|
chat = Chat(
|
||||||
|
chatInfo = ChatInfo.ContactRequest.sampleData,
|
||||||
|
chatItems = listOf(),
|
||||||
|
chatStats = Chat.ChatStats()
|
||||||
|
),
|
||||||
|
click = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,93 +8,128 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.PersonAdd
|
import androidx.compose.material.icons.outlined.*
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.NavOptions
|
|
||||||
import chat.simplex.app.Pages
|
|
||||||
import chat.simplex.app.model.Chat
|
|
||||||
import chat.simplex.app.model.ChatModel
|
import chat.simplex.app.model.ChatModel
|
||||||
import chat.simplex.app.views.helpers.withApi
|
import chat.simplex.app.views.chat.ChatHelpView
|
||||||
import chat.simplex.app.views.newchat.NewChatSheet
|
import chat.simplex.app.views.newchat.NewChatSheet
|
||||||
import chat.simplex.app.views.usersettings.SettingsView
|
import chat.simplex.app.views.usersettings.SettingsView
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@ExperimentalMaterialApi
|
class ScaffoldController(val scope: CoroutineScope) {
|
||||||
class ScaffoldController(val state: BottomSheetScaffoldState, val scope: CoroutineScope) {
|
lateinit var state: BottomSheetScaffoldState
|
||||||
fun expand() = scope.launch { state.bottomSheetState.expand() }
|
val expanded = mutableStateOf(false)
|
||||||
fun collapse() = scope.launch { state.bottomSheetState.collapse() }
|
|
||||||
fun toggle() = scope.launch {
|
fun expand() {
|
||||||
val s = state.bottomSheetState
|
expanded.value = true
|
||||||
if (s.isExpanded) s.collapse() else s.expand()
|
scope.launch { state.bottomSheetState.expand() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun collapse() {
|
||||||
|
expanded.value = false
|
||||||
|
scope.launch { state.bottomSheetState.collapse() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleSheet() {
|
||||||
|
if (state.bottomSheetState.isExpanded) collapse() else expand()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleDrawer() = scope.launch {
|
fun toggleDrawer() = scope.launch {
|
||||||
state.drawerState.apply {
|
state.drawerState.apply { if (isClosed) open() else close() }
|
||||||
if (isClosed) open() else close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExperimentalMaterialApi
|
|
||||||
@Composable
|
@Composable
|
||||||
fun scaffoldController(): ScaffoldController {
|
fun scaffoldController(): ScaffoldController {
|
||||||
return ScaffoldController(
|
val ctrl = ScaffoldController(scope = rememberCoroutineScope())
|
||||||
state = rememberBottomSheetScaffoldState(),
|
val bottomSheetState = rememberBottomSheetState(
|
||||||
scope = rememberCoroutineScope()
|
BottomSheetValue.Collapsed,
|
||||||
|
confirmStateChange = {
|
||||||
|
ctrl.expanded.value = it == BottomSheetValue.Expanded
|
||||||
|
true
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
ctrl.state = rememberBottomSheetScaffoldState(bottomSheetState = bottomSheetState)
|
||||||
|
return ctrl
|
||||||
}
|
}
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
|
||||||
@ExperimentalPermissionsApi
|
|
||||||
@ExperimentalMaterialApi
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatListView(chatModel: ChatModel, nav: NavController) {
|
fun ChatListView(chatModel: ChatModel) {
|
||||||
val scaffoldCtrl = scaffoldController()
|
val scaffoldCtrl = scaffoldController()
|
||||||
BottomSheetScaffold(
|
BottomSheetScaffold(
|
||||||
scaffoldState = scaffoldCtrl.state,
|
scaffoldState = scaffoldCtrl.state,
|
||||||
topBar = {
|
drawerContent = { SettingsView(chatModel) },
|
||||||
ChatListToolbar(
|
|
||||||
scaffoldCtrl,
|
|
||||||
settings = { scaffoldCtrl.toggleDrawer() }
|
|
||||||
)
|
|
||||||
},
|
|
||||||
drawerContent = {
|
|
||||||
SettingsView(chatModel, nav)
|
|
||||||
},
|
|
||||||
sheetPeekHeight = 0.dp,
|
sheetPeekHeight = 0.dp,
|
||||||
sheetContent = { NewChatSheet(chatModel, scaffoldCtrl, nav) },
|
sheetContent = { NewChatSheet(chatModel, scaffoldCtrl) },
|
||||||
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp),
|
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp),
|
||||||
) {
|
) {
|
||||||
Column(
|
Box {
|
||||||
modifier = Modifier
|
Column(
|
||||||
.padding(vertical = 8.dp)
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
|
||||||
.background(MaterialTheme.colors.background)
|
|
||||||
) {
|
|
||||||
ChatList(chatModel, nav)
|
|
||||||
}
|
|
||||||
if (scaffoldCtrl.state.bottomSheetState.isExpanded) {
|
|
||||||
Surface(
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clickable { scaffoldCtrl.collapse() },
|
.background(MaterialTheme.colors.background)
|
||||||
color = Color.Black.copy(alpha = 0.12F)
|
) {
|
||||||
) {}
|
ChatListToolbar(scaffoldCtrl)
|
||||||
|
if (chatModel.chats.isNotEmpty()) {
|
||||||
|
ChatList(chatModel)
|
||||||
|
} else {
|
||||||
|
val user = chatModel.currentUser.value
|
||||||
|
Help(scaffoldCtrl, displayName = user?.profile?.displayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (scaffoldCtrl.expanded.value) {
|
||||||
|
Surface(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clickable { scaffoldCtrl.collapse() },
|
||||||
|
color = Color.Black.copy(alpha = 0.12F)
|
||||||
|
) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExperimentalMaterialApi
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatListToolbar(newChatSheetCtrl: ScaffoldController, settings: () -> Unit) {
|
fun Help(scaffoldCtrl: ScaffoldController, displayName: String?) {
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (displayName != null) "Welcome ${displayName}!" else "Welcome!",
|
||||||
|
Modifier.padding(bottom = 24.dp),
|
||||||
|
style = MaterialTheme.typography.h1,
|
||||||
|
color = MaterialTheme.colors.onBackground
|
||||||
|
)
|
||||||
|
ChatHelpView({ scaffoldCtrl.toggleSheet() })
|
||||||
|
Row(
|
||||||
|
Modifier.padding(top = 30.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"This text is available in settings",
|
||||||
|
color = MaterialTheme.colors.onBackground
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
Icons.Outlined.Settings,
|
||||||
|
"Settings",
|
||||||
|
tint = MaterialTheme.colors.onBackground,
|
||||||
|
modifier = Modifier.clickable(onClick = { scaffoldCtrl.toggleDrawer() })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatListToolbar(scaffoldCtrl: ScaffoldController) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -103,9 +138,9 @@ fun ChatListToolbar(newChatSheetCtrl: ScaffoldController, settings: () -> Unit)
|
|||||||
.padding(horizontal = 8.dp)
|
.padding(horizontal = 8.dp)
|
||||||
.height(60.dp)
|
.height(60.dp)
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = settings) {
|
IconButton(onClick = { scaffoldCtrl.toggleDrawer() }) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.Settings,
|
Icons.Outlined.Menu,
|
||||||
"Settings",
|
"Settings",
|
||||||
tint = MaterialTheme.colors.primary,
|
tint = MaterialTheme.colors.primary,
|
||||||
modifier = Modifier.padding(10.dp)
|
modifier = Modifier.padding(10.dp)
|
||||||
@@ -117,7 +152,7 @@ fun ChatListToolbar(newChatSheetCtrl: ScaffoldController, settings: () -> Unit)
|
|||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
modifier = Modifier.padding(5.dp)
|
modifier = Modifier.padding(5.dp)
|
||||||
)
|
)
|
||||||
IconButton(onClick = { newChatSheetCtrl.toggle() }) {
|
IconButton(onClick = { scaffoldCtrl.toggleSheet() }) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.PersonAdd,
|
Icons.Outlined.PersonAdd,
|
||||||
"Add Contact",
|
"Add Contact",
|
||||||
@@ -128,41 +163,14 @@ fun ChatListToolbar(newChatSheetCtrl: ScaffoldController, settings: () -> Unit)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
|
||||||
fun goToChat(chatPreview: Chat, chatModel: ChatModel, navController: NavController) {
|
|
||||||
withApi {
|
|
||||||
val cInfo = chatPreview.chatInfo
|
|
||||||
val chat = chatModel.controller.apiGetChat(cInfo.chatType, cInfo.apiId)
|
|
||||||
if (chat != null) {
|
|
||||||
chatModel.chatId.value = cInfo.id
|
|
||||||
chatModel.chatItems = chat.chatItems.toMutableStateList()
|
|
||||||
navController.navigate(Pages.Chat.route)
|
|
||||||
} else {
|
|
||||||
// TODO show error? or will apiGetChat show it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatList(chatModel: ChatModel, navController: NavController) {
|
fun ChatList(chatModel: ChatModel) {
|
||||||
|
Divider(Modifier.padding(horizontal = 8.dp))
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
items(chatModel.chats) { chat ->
|
items(chatModel.chats) { chat ->
|
||||||
ChatPreviewView(chat) { goToChat(chat, chatModel, navController) }
|
ChatListNavLinkView(chat, chatModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//@Preview
|
|
||||||
//@Composable
|
|
||||||
//fun PreviewChatListView() {
|
|
||||||
// SimpleXTheme {
|
|
||||||
// ChatListView(
|
|
||||||
// chats = listOf(
|
|
||||||
// Chat()
|
|
||||||
// ),
|
|
||||||
//
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package chat.simplex.app.views.chatlist
|
package chat.simplex.app.views.chatlist
|
||||||
|
|
||||||
import androidx.compose.foundation.*
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -11,91 +13,78 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import chat.simplex.app.model.*
|
import androidx.compose.ui.unit.sp
|
||||||
|
import chat.simplex.app.model.Chat
|
||||||
|
import chat.simplex.app.model.getTimestampText
|
||||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||||
|
import chat.simplex.app.views.chat.item.MarkdownText
|
||||||
import chat.simplex.app.views.helpers.ChatInfoImage
|
import chat.simplex.app.views.helpers.ChatInfoImage
|
||||||
import chat.simplex.app.views.helpers.badgeLayout
|
import chat.simplex.app.views.helpers.badgeLayout
|
||||||
import kotlinx.datetime.Clock
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatPreviewView(chat: Chat, goToChat: () -> Unit) {
|
fun ChatPreviewView(chat: Chat) {
|
||||||
Surface(
|
Row {
|
||||||
border = BorderStroke(0.5.dp, MaterialTheme.colors.secondary),
|
ChatInfoImage(chat, size = 72.dp)
|
||||||
modifier = Modifier
|
Column(
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable(onClick = goToChat)
|
|
||||||
.height(88.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 8.dp)
|
|
||||||
.padding(start = 8.dp)
|
|
||||||
.padding(end = 12.dp),
|
|
||||||
verticalAlignment = Alignment.Top
|
|
||||||
) {
|
|
||||||
ChatInfoImage(chat, size = 72.dp)
|
|
||||||
Column(modifier = Modifier
|
|
||||||
.padding(horizontal = 8.dp)
|
.padding(horizontal = 8.dp)
|
||||||
.weight(1F)) {
|
.weight(1F)
|
||||||
Text(
|
) {
|
||||||
chat.chatInfo.chatViewName,
|
Text(
|
||||||
maxLines = 1,
|
chat.chatInfo.chatViewName,
|
||||||
overflow = TextOverflow.Ellipsis,
|
maxLines = 1,
|
||||||
style = MaterialTheme.typography.h3,
|
overflow = TextOverflow.Ellipsis,
|
||||||
fontWeight = FontWeight.Bold
|
style = MaterialTheme.typography.h3,
|
||||||
)
|
fontWeight = FontWeight.Bold
|
||||||
if (chat.chatItems.count() > 0) {
|
)
|
||||||
Text(
|
|
||||||
chat.chatItems.last().content.text,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.createdAt)
|
|
||||||
Column(Modifier.fillMaxHeight(),
|
|
||||||
verticalArrangement = Arrangement.Top) {
|
|
||||||
Text(ts,
|
|
||||||
color = HighOrLowlight,
|
|
||||||
style = MaterialTheme.typography.body2,
|
|
||||||
modifier = Modifier.padding(bottom=5.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (chat.chatStats.unreadCount > 0) {
|
if (chat.chatItems.count() > 0) {
|
||||||
Text(
|
MarkdownText(
|
||||||
chat.chatStats.unreadCount.toString(),
|
chat.chatItems.last(),
|
||||||
color = MaterialTheme.colors.onPrimary,
|
maxLines = 2,
|
||||||
style = MaterialTheme.typography.body2,
|
overflow = TextOverflow.Ellipsis
|
||||||
modifier = Modifier
|
)
|
||||||
.background(MaterialTheme.colors.primary, shape = CircleShape)
|
}
|
||||||
.align(Alignment.End)
|
}
|
||||||
.badgeLayout()
|
val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.createdAt)
|
||||||
.padding(2.dp)
|
Column(
|
||||||
)
|
Modifier.fillMaxHeight(),
|
||||||
}
|
verticalArrangement = Arrangement.Top
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
ts,
|
||||||
|
color = HighOrLowlight,
|
||||||
|
style = MaterialTheme.typography.body2,
|
||||||
|
modifier = Modifier.padding(bottom = 5.dp)
|
||||||
|
)
|
||||||
|
val n = chat.chatStats.unreadCount
|
||||||
|
if (n > 0) {
|
||||||
|
Text(
|
||||||
|
if (n < 1000) "$n" else "${n / 1000}k",
|
||||||
|
color = MaterialTheme.colors.onPrimary,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
modifier = Modifier
|
||||||
|
.background(MaterialTheme.colors.primary, shape = CircleShape)
|
||||||
|
.align(Alignment.End)
|
||||||
|
.badgeLayout()
|
||||||
|
.padding(horizontal = 3.dp)
|
||||||
|
.padding(vertical = 1.dp)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview(showBackground = true)
|
||||||
|
@Preview(
|
||||||
|
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||||
|
showBackground = true,
|
||||||
|
name = "Dark Mode"
|
||||||
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatPreviewViewExample() {
|
fun PreviewChatPreviewView() {
|
||||||
SimpleXTheme {
|
SimpleXTheme {
|
||||||
ChatPreviewView(
|
ChatPreviewView(Chat.sampleData)
|
||||||
chat = Chat(
|
|
||||||
chatInfo = ChatInfo.Direct.sampleData,
|
|
||||||
chatItems = listOf(ChatItem.getSampleData(
|
|
||||||
1,
|
|
||||||
CIDirection.DirectSnd(),
|
|
||||||
Clock.System.now(),
|
|
||||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
|
||||||
)),
|
|
||||||
chatStats = Chat.ChatStats()
|
|
||||||
),
|
|
||||||
goToChat = {}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package chat.simplex.app.views.chatlist
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import chat.simplex.app.model.Chat
|
||||||
|
import chat.simplex.app.model.getTimestampText
|
||||||
|
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||||
|
import chat.simplex.app.views.helpers.ChatInfoImage
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ContactRequestView(chat: Chat) {
|
||||||
|
Row {
|
||||||
|
ChatInfoImage(chat, size = 72.dp)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.weight(1F)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
chat.chatInfo.chatViewName,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = MaterialTheme.typography.h3,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colors.primary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"wants to connect to you!",
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val ts = getTimestampText(chat.chatInfo.createdAt)
|
||||||
|
Column(
|
||||||
|
Modifier.fillMaxHeight(),
|
||||||
|
verticalArrangement = Arrangement.Top
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
ts,
|
||||||
|
color = HighOrLowlight,
|
||||||
|
style = MaterialTheme.typography.body2,
|
||||||
|
modifier = Modifier.padding(bottom = 5.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package chat.simplex.app.views.helpers
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import chat.simplex.app.TAG
|
||||||
|
|
||||||
|
class AlertManager {
|
||||||
|
var alertView = mutableStateOf<(@Composable () -> Unit)?>(null)
|
||||||
|
var presentAlert = mutableStateOf<Boolean>(false)
|
||||||
|
|
||||||
|
fun showAlert(alert: @Composable () -> Unit) {
|
||||||
|
Log.d(TAG, "AlertManager.showAlert")
|
||||||
|
alertView.value = alert
|
||||||
|
presentAlert.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideAlert() {
|
||||||
|
presentAlert.value = false
|
||||||
|
alertView.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showAlertDialog(
|
||||||
|
title: String,
|
||||||
|
text: String? = null,
|
||||||
|
confirmText: String = "Ok",
|
||||||
|
onConfirm: (() -> Unit)? = null,
|
||||||
|
dismissText: String = "Cancel",
|
||||||
|
onDismiss: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
|
||||||
|
showAlert {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = this::hideAlert,
|
||||||
|
title = { Text(title) },
|
||||||
|
text = alertText,
|
||||||
|
confirmButton = {
|
||||||
|
Button(onClick = {
|
||||||
|
onConfirm?.invoke()
|
||||||
|
hideAlert()
|
||||||
|
}) { Text(confirmText) }
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
Button(onClick = {
|
||||||
|
onDismiss?.invoke()
|
||||||
|
hideAlert()
|
||||||
|
}) { Text(dismissText) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showAlertMsg(
|
||||||
|
title: String, text: String? = null,
|
||||||
|
confirmText: String = "Ok", onConfirm: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
|
||||||
|
showAlert {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = this::hideAlert,
|
||||||
|
title = { Text(title) },
|
||||||
|
text = alertText,
|
||||||
|
confirmButton = {
|
||||||
|
Button(onClick = {
|
||||||
|
onConfirm?.invoke()
|
||||||
|
hideAlert()
|
||||||
|
}) { Text(confirmText) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun showInView() {
|
||||||
|
if (presentAlert.value) alertView.value?.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val shared = AlertManager()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package chat.simplex.app.views.helpers
|
package chat.simplex.app.views.helpers
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -9,8 +8,6 @@ import androidx.compose.material.icons.filled.AccountCircle
|
|||||||
import androidx.compose.material.icons.filled.SupervisedUserCircle
|
import androidx.compose.material.icons.filled.SupervisedUserCircle
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package chat.simplex.app.views.newchat
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Surface
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import chat.simplex.app.TAG
|
||||||
|
import chat.simplex.app.views.helpers.CloseSheetBar
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ModalView(close: () -> Unit, content: @Composable () -> Unit) {
|
||||||
|
BackHandler(onBack = close)
|
||||||
|
Surface(
|
||||||
|
Modifier
|
||||||
|
.background(MaterialTheme.colors.background)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
CloseSheetBar(close)
|
||||||
|
Box(Modifier.padding(horizontal = 16.dp)) { content() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModalManager {
|
||||||
|
private val modalViews = arrayListOf<(@Composable (close: () -> Unit) -> Unit)?>()
|
||||||
|
private val modalCount = mutableStateOf(0)
|
||||||
|
|
||||||
|
fun showModal(content: @Composable () -> Unit) {
|
||||||
|
showCustomModal { close -> ModalView(close, content) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showCustomModal(modal: @Composable (close: () -> Unit) -> Unit) {
|
||||||
|
Log.d(TAG, "ModalManager.showModal")
|
||||||
|
modalViews.add(modal)
|
||||||
|
modalCount.value = modalViews.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeModal() {
|
||||||
|
if (!modalViews.isEmpty()) {
|
||||||
|
modalViews.removeAt(modalViews.count() - 1)
|
||||||
|
}
|
||||||
|
modalCount.value = modalViews.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun showInView() {
|
||||||
|
if (modalCount.value > 0) modalViews.lastOrNull()?.invoke(::closeModal)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val shared = ModalManager()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package chat.simplex.app.views.helpers
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
|
||||||
|
fun shareText(cxt: Context, text: String) {
|
||||||
|
val sendIntent: Intent = Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND
|
||||||
|
putExtra(Intent.EXTRA_TEXT, text)
|
||||||
|
type = "text/plain"
|
||||||
|
}
|
||||||
|
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||||
|
cxt.startActivity(shareIntent)
|
||||||
|
}
|
||||||
@@ -1,7 +1,40 @@
|
|||||||
package chat.simplex.app.views.helpers
|
package chat.simplex.app.views.helpers
|
||||||
|
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.view.ViewTreeObserver
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
|
||||||
fun withApi(action: suspend CoroutineScope.() -> Unit): Job =
|
fun withApi(action: suspend CoroutineScope.() -> Unit): Job =
|
||||||
GlobalScope.launch { withContext(Dispatchers.Main, action) }
|
GlobalScope.launch { withContext(Dispatchers.Main, action) }
|
||||||
|
|
||||||
|
enum class KeyboardState {
|
||||||
|
Opened, Closed
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun getKeyboardState(): State<KeyboardState> {
|
||||||
|
val keyboardState = remember { mutableStateOf(KeyboardState.Closed) }
|
||||||
|
val view = LocalView.current
|
||||||
|
DisposableEffect(view) {
|
||||||
|
val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {
|
||||||
|
val rect = Rect()
|
||||||
|
view.getWindowVisibleDisplayFrame(rect)
|
||||||
|
val screenHeight = view.rootView.height
|
||||||
|
val keypadHeight = screenHeight - rect.bottom
|
||||||
|
keyboardState.value = if (keypadHeight > screenHeight * 0.15) {
|
||||||
|
KeyboardState.Opened
|
||||||
|
} else {
|
||||||
|
KeyboardState.Closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener)
|
||||||
|
|
||||||
|
onDispose {
|
||||||
|
view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyboardState
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package chat.simplex.app.views.newchat
|
package chat.simplex.app.views.newchat
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.res.Configuration
|
||||||
import android.content.Intent
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
@@ -17,83 +15,78 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
import androidx.compose.ui.unit.sp
|
||||||
import chat.simplex.app.model.ChatModel
|
import chat.simplex.app.model.ChatModel
|
||||||
import chat.simplex.app.ui.theme.SimpleButton
|
import chat.simplex.app.ui.theme.SimpleButton
|
||||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||||
import chat.simplex.app.views.helpers.CloseSheetBar
|
import chat.simplex.app.views.helpers.shareText
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AddContactView(chatModel: ChatModel, nav: NavController) {
|
fun AddContactView(chatModel: ChatModel) {
|
||||||
val connReq = chatModel.connReqInvitation
|
val connReq = chatModel.connReqInvitation
|
||||||
if (connReq != null) {
|
if (connReq != null) {
|
||||||
val cxt = LocalContext.current
|
val cxt = LocalContext.current
|
||||||
AddContactLayout(
|
AddContactLayout(
|
||||||
connReq = connReq,
|
connReq = connReq,
|
||||||
close = { nav.popBackStack() },
|
|
||||||
share = { shareText(cxt, connReq) }
|
share = { shareText(cxt, connReq) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AddContactLayout(connReq: String, close: () -> Unit, share: () -> Unit) {
|
fun AddContactLayout(connReq: String, share: () -> Unit) {
|
||||||
Column(
|
BoxWithConstraints {
|
||||||
modifier = Modifier
|
val screenHeight = maxHeight
|
||||||
.padding(horizontal = 8.dp)
|
Column(
|
||||||
.fillMaxSize()
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
.background(MaterialTheme.colors.background),
|
verticalArrangement = Arrangement.SpaceBetween,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
) {
|
||||||
) {
|
Text(
|
||||||
CloseSheetBar(close)
|
"Add contact",
|
||||||
Text(
|
style = MaterialTheme.typography.h1,
|
||||||
"Add contact",
|
)
|
||||||
style = MaterialTheme.typography.h1,
|
Text(
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
"Show QR code to your contact\nto scan from the app",
|
||||||
)
|
style = MaterialTheme.typography.h2.copy(fontSize = if(screenHeight > 600.dp) 26.sp else 20.sp),
|
||||||
Text(
|
textAlign = TextAlign.Center,
|
||||||
"Show QR code to your contact\nto scan from the app",
|
)
|
||||||
style = MaterialTheme.typography.h2,
|
QRCode(
|
||||||
textAlign = TextAlign.Center,
|
connReq, Modifier
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
.weight(1f, fill = false)
|
||||||
)
|
.aspectRatio(1f)
|
||||||
QRCode(connReq)
|
.padding(vertical = 3.dp)
|
||||||
Text(
|
)
|
||||||
buildAnnotatedString {
|
Text(
|
||||||
append("If you cannot meet in person, you can ")
|
buildAnnotatedString {
|
||||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
append("If you cannot meet in person, you can ")
|
||||||
append("scan QR code in the video call")
|
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||||
}
|
append("scan QR code in the video call")
|
||||||
append(", or you can share the invitation link via any other channel.")
|
}
|
||||||
},
|
append(", or you can share the invitation link via any other channel.")
|
||||||
textAlign = TextAlign.Center,
|
},
|
||||||
style = MaterialTheme.typography.caption,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier
|
style = MaterialTheme.typography.caption.copy(fontSize=if(screenHeight > 600.dp) 20.sp else 16.sp),
|
||||||
.padding(horizontal = 16.dp)
|
modifier = Modifier
|
||||||
.padding(top = 8.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.padding(bottom = 16.dp)
|
.padding(bottom = if(screenHeight > 600.dp) 16.dp else 8.dp)
|
||||||
)
|
)
|
||||||
SimpleButton("Share invitation link", icon = Icons.Outlined.Share, click = share)
|
SimpleButton("Share invitation link", icon = Icons.Outlined.Share, click = share)
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shareText(cxt: Context, text: String) {
|
|
||||||
val sendIntent: Intent = Intent().apply {
|
|
||||||
action = Intent.ACTION_SEND
|
|
||||||
putExtra(Intent.EXTRA_TEXT, text)
|
|
||||||
type = "text/plain"
|
|
||||||
}
|
|
||||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
|
||||||
cxt.startActivity(shareIntent)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
|
@Preview(
|
||||||
|
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||||
|
showBackground = true,
|
||||||
|
name = "Dark Mode"
|
||||||
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun PreviewAddContactView() {
|
fun PreviewAddContactView() {
|
||||||
SimpleXTheme {
|
SimpleXTheme {
|
||||||
AddContactLayout(
|
AddContactLayout(
|
||||||
connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D",
|
connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D",
|
||||||
close = {},
|
|
||||||
share = {}
|
share = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
package chat.simplex.app.views.newchat
|
package chat.simplex.app.views.newchat
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.foundation.background
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -12,45 +13,41 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
|
||||||
import chat.simplex.app.model.ChatModel
|
import chat.simplex.app.model.ChatModel
|
||||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||||
import chat.simplex.app.views.helpers.CloseSheetBar
|
import chat.simplex.app.views.helpers.AlertManager
|
||||||
import chat.simplex.app.views.helpers.withApi
|
import chat.simplex.app.views.helpers.withApi
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConnectContactView(chatModel: ChatModel, nav: NavController) {
|
fun ConnectContactView(chatModel: ChatModel, close: () -> Unit) {
|
||||||
|
BackHandler(onBack = close)
|
||||||
ConnectContactLayout(
|
ConnectContactLayout(
|
||||||
qrCodeScanner = {
|
qrCodeScanner = {
|
||||||
QRCodeScanner { connReqUri ->
|
QRCodeScanner { connReqUri ->
|
||||||
try {
|
try {
|
||||||
val uri = Uri.parse(connReqUri)
|
val uri = Uri.parse(connReqUri)
|
||||||
withUriAction(chatModel, uri) { action ->
|
withUriAction(uri) { action ->
|
||||||
connectViaUri(chatModel, action, uri)
|
connectViaUri(chatModel, action, uri)
|
||||||
}
|
}
|
||||||
} catch(e: RuntimeException) {
|
} catch (e: RuntimeException) {
|
||||||
chatModel.alertManager.showAlertMsg(
|
AlertManager.shared.showAlertMsg(
|
||||||
title = "Invalid QR code",
|
title = "Invalid QR code",
|
||||||
text = "This QR code is not a link!"
|
text = "This QR code is not a link!"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
nav.popBackStack()
|
close()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
close = { nav.popBackStack() }
|
close = close
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
fun withUriAction(uri: Uri, run: suspend (String) -> Unit) {
|
||||||
fun withUriAction(chatModel: ChatModel, uri: Uri,
|
|
||||||
run: suspend (String) -> Unit) {
|
|
||||||
val action = uri.path?.drop(1)
|
val action = uri.path?.drop(1)
|
||||||
if (action == "contact" || action == "invitation") {
|
if (action == "contact" || action == "invitation") {
|
||||||
withApi { run(action) }
|
withApi { run(action) }
|
||||||
} else {
|
} else {
|
||||||
chatModel.alertManager.showAlertMsg(
|
AlertManager.shared.showAlertMsg(
|
||||||
title = "Invalid link!",
|
title = "Invalid link!",
|
||||||
text = "This link is not a valid connection link!"
|
text = "This link is not a valid connection link!"
|
||||||
)
|
)
|
||||||
@@ -63,7 +60,7 @@ suspend fun connectViaUri(chatModel: ChatModel, action: String, uri: Uri) {
|
|||||||
val whenConnected =
|
val whenConnected =
|
||||||
if (action == "contact") "your connection request is accepted"
|
if (action == "contact") "your connection request is accepted"
|
||||||
else "your contact's device is online"
|
else "your contact's device is online"
|
||||||
chatModel.alertManager.showAlertMsg(
|
AlertManager.shared.showAlertMsg(
|
||||||
title = "Connection request sent!",
|
title = "Connection request sent!",
|
||||||
text = "You will be connected when $whenConnected, please wait or check later!"
|
text = "You will be connected when $whenConnected, please wait or check later!"
|
||||||
)
|
)
|
||||||
@@ -72,48 +69,50 @@ suspend fun connectViaUri(chatModel: ChatModel, action: String, uri: Uri) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConnectContactLayout(qrCodeScanner: @Composable () -> Unit, close: () -> Unit) {
|
fun ConnectContactLayout(qrCodeScanner: @Composable () -> Unit, close: () -> Unit) {
|
||||||
Column(
|
ModalView(close) {
|
||||||
modifier = Modifier
|
Column(
|
||||||
.padding(horizontal = 8.dp)
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
.fillMaxSize()
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
.background(MaterialTheme.colors.background),
|
) {
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
Text(
|
||||||
) {
|
"Scan QR code",
|
||||||
CloseSheetBar(close)
|
style = MaterialTheme.typography.h1,
|
||||||
Text(
|
)
|
||||||
"Scan QR code",
|
Text(
|
||||||
style = MaterialTheme.typography.h1,
|
"Your chat profile will be sent\nto your contact",
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
style = MaterialTheme.typography.h2,
|
||||||
)
|
textAlign = TextAlign.Center,
|
||||||
Text(
|
modifier = Modifier.padding(bottom = 4.dp)
|
||||||
"Your chat profile will be sent\nto your contact",
|
)
|
||||||
style = MaterialTheme.typography.h2,
|
Box(
|
||||||
textAlign = TextAlign.Center,
|
Modifier
|
||||||
modifier = Modifier.padding(bottom = 16.dp)
|
.fillMaxWidth()
|
||||||
)
|
.aspectRatio(ratio = 1F)
|
||||||
Box (
|
) { qrCodeScanner() }
|
||||||
Modifier
|
Text(
|
||||||
.fillMaxWidth()
|
buildAnnotatedString {
|
||||||
.aspectRatio(ratio = 1F)
|
append("If you cannot meet in person, you can ")
|
||||||
) { qrCodeScanner() }
|
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||||
Text(
|
append("scan QR code in the video call")
|
||||||
buildAnnotatedString {
|
}
|
||||||
append("If you cannot meet in person, you can ")
|
append(", or you can create the invitation link.")
|
||||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
},
|
||||||
append("scan QR code in the video call")
|
textAlign = TextAlign.Center,
|
||||||
}
|
style = MaterialTheme.typography.caption,
|
||||||
append(", or you can create the invitation link.")
|
modifier = Modifier
|
||||||
},
|
.padding(horizontal = 16.dp)
|
||||||
textAlign = TextAlign.Center,
|
.padding(top = 4.dp)
|
||||||
style = MaterialTheme.typography.caption,
|
)
|
||||||
modifier = Modifier
|
}
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.padding(top = 16.dp)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
|
@Preview(
|
||||||
|
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||||
|
showBackground = true,
|
||||||
|
name = "Dark Mode"
|
||||||
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun PreviewConnectContactLayout() {
|
fun PreviewConnectContactLayout() {
|
||||||
SimpleXTheme {
|
SimpleXTheme {
|
||||||
|
|||||||
@@ -14,22 +14,15 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
|
||||||
import chat.simplex.app.Pages
|
|
||||||
import chat.simplex.app.model.ChatModel
|
import chat.simplex.app.model.ChatModel
|
||||||
import chat.simplex.app.ui.theme.DarkGray
|
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||||
import chat.simplex.app.views.chatlist.ScaffoldController
|
import chat.simplex.app.views.chatlist.ScaffoldController
|
||||||
import chat.simplex.app.views.helpers.withApi
|
import chat.simplex.app.views.helpers.withApi
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
|
||||||
@ExperimentalPermissionsApi
|
|
||||||
@ExperimentalMaterialApi
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NewChatSheet(chatModel: ChatModel, newChatCtrl: ScaffoldController, nav: NavController) {
|
fun NewChatSheet(chatModel: ChatModel, newChatCtrl: ScaffoldController) {
|
||||||
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
|
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
|
||||||
NewChatSheetLayout(
|
NewChatSheetLayout(
|
||||||
addContact = {
|
addContact = {
|
||||||
@@ -39,41 +32,48 @@ fun NewChatSheet(chatModel: ChatModel, newChatCtrl: ScaffoldController, nav: Nav
|
|||||||
// hide spinner
|
// hide spinner
|
||||||
if (chatModel.connReqInvitation != null) {
|
if (chatModel.connReqInvitation != null) {
|
||||||
newChatCtrl.collapse()
|
newChatCtrl.collapse()
|
||||||
nav.navigate(Pages.AddContact.route)
|
ModalManager.shared.showModal { AddContactView(chatModel) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scanCode = {
|
scanCode = {
|
||||||
newChatCtrl.collapse()
|
newChatCtrl.collapse()
|
||||||
nav.navigate(Pages.Connect.route)
|
ModalManager.shared.showCustomModal { close -> ConnectContactView(chatModel, close) }
|
||||||
cameraPermissionState.launchPermissionRequest()
|
cameraPermissionState.launchPermissionRequest()
|
||||||
},
|
|
||||||
close = {
|
|
||||||
newChatCtrl.collapse()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NewChatSheetLayout(addContact: () -> Unit, scanCode: () -> Unit, close: () -> Unit) {
|
fun NewChatSheetLayout(addContact: () -> Unit, scanCode: () -> Unit) {
|
||||||
Row(Modifier
|
Row(
|
||||||
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 8.dp, vertical = 48.dp),
|
.padding(horizontal = 8.dp, vertical = 48.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
) {
|
) {
|
||||||
Box(Modifier.weight(1F).fillMaxWidth()) {
|
Box(
|
||||||
|
Modifier
|
||||||
|
.weight(1F)
|
||||||
|
.fillMaxWidth()) {
|
||||||
ActionButton(
|
ActionButton(
|
||||||
"Add contact", "(create QR code\nor link)",
|
"Add contact", "(create QR code\nor link)",
|
||||||
Icons.Outlined.PersonAdd, click = addContact
|
Icons.Outlined.PersonAdd, click = addContact
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Box(Modifier.weight(1F).fillMaxWidth()) {
|
Box(
|
||||||
|
Modifier
|
||||||
|
.weight(1F)
|
||||||
|
.fillMaxWidth()) {
|
||||||
ActionButton(
|
ActionButton(
|
||||||
"Scan QR code", "(in person or in video call)",
|
"Scan QR code", "(in person or in video call)",
|
||||||
Icons.Outlined.QrCode, click = scanCode
|
Icons.Outlined.QrCode, click = scanCode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Box(Modifier.weight(1F).fillMaxWidth()) {
|
Box(
|
||||||
|
Modifier
|
||||||
|
.weight(1F)
|
||||||
|
.fillMaxWidth()) {
|
||||||
ActionButton(
|
ActionButton(
|
||||||
"Create Group", "(coming soon!)",
|
"Create Group", "(coming soon!)",
|
||||||
Icons.Outlined.GroupAdd, disabled = true
|
Icons.Outlined.GroupAdd, disabled = true
|
||||||
@@ -91,7 +91,7 @@ fun ActionButton(text: String, comment: String, icon: ImageVector, disabled: Boo
|
|||||||
.padding(horizontal = 8.dp),
|
.padding(horizontal = 8.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
val tint = if (disabled) DarkGray else MaterialTheme.colors.primary
|
val tint = if (disabled) HighOrLowlight else MaterialTheme.colors.primary
|
||||||
Icon(icon, text,
|
Icon(icon, text,
|
||||||
tint = tint,
|
tint = tint,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -116,8 +116,7 @@ fun PreviewNewChatSheet() {
|
|||||||
SimpleXTheme {
|
SimpleXTheme {
|
||||||
NewChatSheetLayout(
|
NewChatSheetLayout(
|
||||||
addContact = {},
|
addContact = {},
|
||||||
scanCode = {},
|
scanCode = {}
|
||||||
close = {},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.graphics.Bitmap
|
|||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||||
@@ -12,10 +13,11 @@ import com.google.zxing.EncodeHintType
|
|||||||
import com.google.zxing.qrcode.QRCodeWriter
|
import com.google.zxing.qrcode.QRCodeWriter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun QRCode(connReq: String) {
|
fun QRCode(connReq: String, modifier: Modifier = Modifier) {
|
||||||
Image(
|
Image(
|
||||||
bitmap = qrCodeBitmap(connReq, 1024).asImageBitmap(),
|
bitmap = qrCodeBitmap(connReq, 1024).asImageBitmap(),
|
||||||
contentDescription = "QR Code"
|
contentDescription = "QR Code",
|
||||||
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import chat.simplex.app.TAG
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
|
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
|
||||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||||
@@ -62,7 +63,7 @@ fun QRCodeScanner(onBarcode: (String) -> Unit) {
|
|||||||
cameraProvider.unbindAll()
|
cameraProvider.unbindAll()
|
||||||
cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis)
|
cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d("SIMPLEX", "CameraPreview: ${e.localizedMessage}")
|
Log.d(TAG, "CameraPreview: ${e.localizedMessage}")
|
||||||
}
|
}
|
||||||
}, ContextCompat.getMainExecutor(context))
|
}, ContextCompat.getMainExecutor(context))
|
||||||
}
|
}
|
||||||
@@ -90,11 +91,11 @@ class BarCodeAnalyser(
|
|||||||
if (barcodes.isNotEmpty()) {
|
if (barcodes.isNotEmpty()) {
|
||||||
onBarcodeDetected(barcodes)
|
onBarcodeDetected(barcodes)
|
||||||
} else {
|
} else {
|
||||||
Log.d("SIMPLEX", "BarcodeAnalyser: No barcode Scanned")
|
Log.d(TAG, "BarcodeAnalyser: No barcode Scanned")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.addOnFailureListener { exception ->
|
.addOnFailureListener { exception ->
|
||||||
Log.d("SIMPLEX", "BarcodeAnalyser: Something went wrong $exception")
|
Log.e(TAG, "BarcodeAnalyser: Something went wrong $exception")
|
||||||
}
|
}
|
||||||
.addOnCompleteListener {
|
.addOnCompleteListener {
|
||||||
image.close()
|
image.close()
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package chat.simplex.app.views.usersettings
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import chat.simplex.app.model.ChatModel
|
||||||
|
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||||
|
import chat.simplex.app.views.chat.ChatHelpView
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HelpView(chatModel: ChatModel) {
|
||||||
|
val user = chatModel.currentUser.value
|
||||||
|
if (user != null) {
|
||||||
|
HelpLayout(displayName = user.profile.displayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HelpLayout(displayName: String) {
|
||||||
|
Column(horizontalAlignment = Alignment.Start) {
|
||||||
|
Text(
|
||||||
|
"Welcome $displayName!",
|
||||||
|
Modifier.padding(bottom = 24.dp),
|
||||||
|
style = MaterialTheme.typography.h1,
|
||||||
|
)
|
||||||
|
ChatHelpView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Preview(
|
||||||
|
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||||
|
showBackground = true,
|
||||||
|
name = "Dark Mode"
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
fun PreviewHelpView() {
|
||||||
|
SimpleXTheme {
|
||||||
|
HelpLayout(displayName = "Alice")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package chat.simplex.app.views.usersettings
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.*
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import chat.simplex.app.model.Format
|
||||||
|
import chat.simplex.app.model.FormatColor
|
||||||
|
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MarkdownHelpView() {
|
||||||
|
Column(Modifier.padding(horizontal = 16.dp)) {
|
||||||
|
Text(
|
||||||
|
"How to use markdown",
|
||||||
|
style = MaterialTheme.typography.h1,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"You can use markdown to format messages:",
|
||||||
|
Modifier.padding(vertical = 16.dp)
|
||||||
|
)
|
||||||
|
MdFormat("*bold*", "bold", Format.Bold())
|
||||||
|
MdFormat("_italic_", "italic", Format.Italic())
|
||||||
|
MdFormat("~strike~", "strike", Format.StrikeThrough())
|
||||||
|
MdFormat("`a + b`", "a + b", Format.Snippet())
|
||||||
|
Row {
|
||||||
|
MdSyntax("!1 colored!")
|
||||||
|
Text(buildAnnotatedString {
|
||||||
|
withStyle(Format.Colored(FormatColor.red).style) { append("colored") }
|
||||||
|
append(" (")
|
||||||
|
appendColor(this, "1", FormatColor.red, ", ")
|
||||||
|
appendColor(this, "2", FormatColor.green, ", ")
|
||||||
|
appendColor(this, "3", FormatColor.blue, ", ")
|
||||||
|
appendColor(this, "4", FormatColor.yellow, ", ")
|
||||||
|
appendColor(this, "5", FormatColor.cyan, ", ")
|
||||||
|
appendColor(this, "6", FormatColor.magenta, ")")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
MdSyntax("#secret#")
|
||||||
|
SelectionContainer {
|
||||||
|
Text(buildAnnotatedString {
|
||||||
|
withStyle(Format.Secret().style) { append("secret") }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MdSyntax(markdown: String) {
|
||||||
|
Text(markdown, Modifier
|
||||||
|
.width(100.dp)
|
||||||
|
.padding(bottom = 4.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MdFormat(markdown: String, example: String, format: Format) {
|
||||||
|
Row {
|
||||||
|
MdSyntax(markdown)
|
||||||
|
Text(buildAnnotatedString {
|
||||||
|
withStyle(format.style) { append(example) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun appendColor(b: AnnotatedString.Builder, s: String, c: FormatColor, after: String) {
|
||||||
|
b.withStyle(Format.Colored(c).style) { append(s)}
|
||||||
|
b.append(after)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Preview(
|
||||||
|
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||||
|
showBackground = true,
|
||||||
|
name = "Dark Mode"
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
fun PreviewMarkdownHelpView() {
|
||||||
|
SimpleXTheme {
|
||||||
|
MarkdownHelpView()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
package chat.simplex.app.views.usersettings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.*
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.OpenInNew
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import chat.simplex.app.model.ChatModel
|
||||||
|
import chat.simplex.app.ui.theme.*
|
||||||
|
import chat.simplex.app.views.helpers.AlertManager
|
||||||
|
import chat.simplex.app.views.helpers.withApi
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SMPServersView(chatModel: ChatModel) {
|
||||||
|
val userSMPServers = chatModel.userSMPServers.value
|
||||||
|
if (userSMPServers != null) {
|
||||||
|
var isUserSMPServers by remember { mutableStateOf(userSMPServers.isNotEmpty()) }
|
||||||
|
var editSMPServers by remember { mutableStateOf(!isUserSMPServers) }
|
||||||
|
var userSMPServersStr by remember { mutableStateOf(if (isUserSMPServers) userSMPServers.joinToString(separator = "\n") else "") }
|
||||||
|
fun saveSMPServers(smpServers: List<String>) {
|
||||||
|
withApi {
|
||||||
|
val r = chatModel.controller.setUserSMPServers(smpServers = smpServers)
|
||||||
|
if (r) {
|
||||||
|
chatModel.userSMPServers.value = smpServers
|
||||||
|
if (smpServers.isEmpty()) {
|
||||||
|
isUserSMPServers = false
|
||||||
|
editSMPServers = true
|
||||||
|
} else {
|
||||||
|
editSMPServers = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SMPServersLayout(
|
||||||
|
isUserSMPServers = isUserSMPServers,
|
||||||
|
editSMPServers = editSMPServers,
|
||||||
|
userSMPServersStr = userSMPServersStr,
|
||||||
|
isUserSMPServersOnOff = { switch ->
|
||||||
|
if (switch) {
|
||||||
|
isUserSMPServers = true
|
||||||
|
} else {
|
||||||
|
val userSMPServers = chatModel.userSMPServers.value
|
||||||
|
if (userSMPServers != null) {
|
||||||
|
if (userSMPServers.isNotEmpty()) {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
title = "Use SimpleX Chat servers?",
|
||||||
|
text = "Saved SMP servers will be removed",
|
||||||
|
confirmText = "Confirm",
|
||||||
|
onConfirm = {
|
||||||
|
saveSMPServers(listOf())
|
||||||
|
isUserSMPServers = false
|
||||||
|
userSMPServersStr = ""
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
isUserSMPServers = false
|
||||||
|
userSMPServersStr = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
editUserSMPServersStr = { userSMPServersStr = it },
|
||||||
|
cancelEdit = {
|
||||||
|
val userSMPServers = chatModel.userSMPServers.value
|
||||||
|
if (userSMPServers != null) {
|
||||||
|
isUserSMPServers = userSMPServers.isNotEmpty()
|
||||||
|
editSMPServers = !isUserSMPServers
|
||||||
|
userSMPServersStr = if (isUserSMPServers) userSMPServers.joinToString(separator = "\n") else ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
saveSMPServers = { saveSMPServers(it) },
|
||||||
|
editOn = { editSMPServers = true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SMPServersLayout(
|
||||||
|
isUserSMPServers: Boolean,
|
||||||
|
editSMPServers: Boolean,
|
||||||
|
userSMPServersStr: String,
|
||||||
|
isUserSMPServersOnOff: (Boolean) -> Unit,
|
||||||
|
editUserSMPServersStr: (String) -> Unit,
|
||||||
|
cancelEdit: () -> Unit,
|
||||||
|
saveSMPServers: (List<String>) -> Unit,
|
||||||
|
editOn: () -> Unit,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Your SMP servers",
|
||||||
|
Modifier.padding(bottom = 24.dp),
|
||||||
|
style = MaterialTheme.typography.h1
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text("Configure SMP servers", Modifier.padding(end = 24.dp))
|
||||||
|
Switch(
|
||||||
|
checked = isUserSMPServers,
|
||||||
|
onCheckedChange = isUserSMPServersOnOff,
|
||||||
|
colors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = MaterialTheme.colors.primary,
|
||||||
|
uncheckedThumbColor = HighOrLowlight
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isUserSMPServers) {
|
||||||
|
Text("Using SimpleX Chat servers.")
|
||||||
|
} else {
|
||||||
|
Text("Enter one SMP server per line:")
|
||||||
|
if (editSMPServers) {
|
||||||
|
BasicTextField(
|
||||||
|
value = userSMPServersStr,
|
||||||
|
onValueChange = editUserSMPServersStr,
|
||||||
|
textStyle = TextStyle(
|
||||||
|
fontFamily = FontFamily.Monospace, fontSize = 14.sp,
|
||||||
|
color = MaterialTheme.colors.onBackground
|
||||||
|
),
|
||||||
|
keyboardOptions = KeyboardOptions.Default.copy(
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
autoCorrect = false
|
||||||
|
),
|
||||||
|
modifier = Modifier.height(160.dp),
|
||||||
|
cursorBrush = SolidColor(HighOrLowlight),
|
||||||
|
decorationBox = { innerTextField ->
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(10.dp),
|
||||||
|
border = BorderStroke(1.dp, MaterialTheme.colors.secondary)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
Modifier.background(MaterialTheme.colors.background),
|
||||||
|
verticalAlignment = Alignment.Top
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(vertical = 5.dp, horizontal = 7.dp)
|
||||||
|
) {
|
||||||
|
innerTextField()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(horizontalAlignment = Alignment.Start) {
|
||||||
|
Row {
|
||||||
|
Text(
|
||||||
|
"Cancel",
|
||||||
|
color = MaterialTheme.colors.primary,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = cancelEdit)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.padding(horizontal = 8.dp))
|
||||||
|
Text(
|
||||||
|
"Save",
|
||||||
|
color = MaterialTheme.colors.primary,
|
||||||
|
modifier = Modifier.clickable(onClick = {
|
||||||
|
val servers = userSMPServersStr.split("\n")
|
||||||
|
saveSMPServers(servers)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column(horizontalAlignment = Alignment.End) {
|
||||||
|
howToButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(160.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(10.dp),
|
||||||
|
border = BorderStroke(1.dp, MaterialTheme.colors.secondary)
|
||||||
|
) {
|
||||||
|
SelectionContainer(
|
||||||
|
Modifier.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
userSMPServersStr,
|
||||||
|
Modifier
|
||||||
|
.padding(vertical = 5.dp, horizontal = 7.dp),
|
||||||
|
style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 14.sp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(horizontalAlignment = Alignment.Start) {
|
||||||
|
Text(
|
||||||
|
"Edit",
|
||||||
|
color = MaterialTheme.colors.primary,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = editOn)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(horizontalAlignment = Alignment.End) {
|
||||||
|
howToButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun howToButton() {
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.clickable { uriHandler.openUri("https://github.com/simplex-chat/simplexmq#using-smp-server-and-smp-agent") }
|
||||||
|
) {
|
||||||
|
Text("How to", color = MaterialTheme.colors.primary)
|
||||||
|
Icon(
|
||||||
|
Icons.Outlined.OpenInNew, "How to", tint = MaterialTheme.colors.primary,
|
||||||
|
modifier = Modifier.padding(horizontal = 5.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun PreviewSMPServersLayoutDefaultServers() {
|
||||||
|
SimpleXTheme {
|
||||||
|
SMPServersLayout(
|
||||||
|
isUserSMPServers = false,
|
||||||
|
editSMPServers = true,
|
||||||
|
userSMPServersStr = "",
|
||||||
|
isUserSMPServersOnOff = {},
|
||||||
|
editUserSMPServersStr = {},
|
||||||
|
cancelEdit = {},
|
||||||
|
saveSMPServers = {},
|
||||||
|
editOn = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun PreviewSMPServersLayoutUserServersEditOn() {
|
||||||
|
SimpleXTheme {
|
||||||
|
SMPServersLayout(
|
||||||
|
isUserSMPServers = true,
|
||||||
|
editSMPServers = true,
|
||||||
|
userSMPServersStr = "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im\nsmp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im",
|
||||||
|
isUserSMPServersOnOff = {},
|
||||||
|
editUserSMPServersStr = {},
|
||||||
|
cancelEdit = {},
|
||||||
|
saveSMPServers = {},
|
||||||
|
editOn = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun PreviewSMPServersLayoutUserServersEditOff() {
|
||||||
|
SimpleXTheme {
|
||||||
|
SMPServersLayout(
|
||||||
|
isUserSMPServers = true,
|
||||||
|
editSMPServers = false,
|
||||||
|
userSMPServersStr = "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im\nsmp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im",
|
||||||
|
isUserSMPServersOnOff = {},
|
||||||
|
editUserSMPServersStr = {},
|
||||||
|
cancelEdit = {},
|
||||||
|
saveSMPServers = {},
|
||||||
|
editOn = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package chat.simplex.app.views.usersettings
|
package chat.simplex.app.views.usersettings
|
||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
@@ -11,25 +12,26 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.*
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
|
||||||
import chat.simplex.app.Pages
|
|
||||||
import chat.simplex.app.R
|
import chat.simplex.app.R
|
||||||
import chat.simplex.app.model.ChatModel
|
import chat.simplex.app.model.ChatModel
|
||||||
import chat.simplex.app.model.Profile
|
import chat.simplex.app.model.Profile
|
||||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
|
||||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||||
|
import chat.simplex.app.views.TerminalView
|
||||||
|
import chat.simplex.app.views.newchat.ModalManager
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsView(chatModel: ChatModel, nav: NavController) {
|
fun SettingsView(chatModel: ChatModel) {
|
||||||
val user = chatModel.currentUser.value
|
val user = chatModel.currentUser.value
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
SettingsLayout(
|
SettingsLayout(
|
||||||
profile = user.profile,
|
profile = user.profile,
|
||||||
navigate = nav::navigate
|
showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } },
|
||||||
|
showTerminal = { ModalManager.shared.showCustomModal { close -> TerminalView(chatModel, close) } }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,28 +42,33 @@ val simplexTeamUri =
|
|||||||
@Composable
|
@Composable
|
||||||
fun SettingsLayout(
|
fun SettingsLayout(
|
||||||
profile: Profile,
|
profile: Profile,
|
||||||
navigate: (String) -> Unit
|
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||||
|
showTerminal: () -> Unit
|
||||||
) {
|
) {
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
Column(
|
Surface(
|
||||||
Modifier
|
Modifier
|
||||||
|
.background(MaterialTheme.colors.background)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
// .background(MaterialTheme.colors.background)
|
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Column(
|
||||||
"Your Settings",
|
Modifier
|
||||||
style = MaterialTheme.typography.h1,
|
.fillMaxSize()
|
||||||
color = MaterialTheme.colors.onBackground
|
.background(MaterialTheme.colors.background)
|
||||||
)
|
.padding(8.dp)
|
||||||
Spacer(Modifier.height(30.dp))
|
.padding(top = 16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Your Settings",
|
||||||
|
style = MaterialTheme.typography.h1,
|
||||||
|
modifier = Modifier.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(30.dp))
|
||||||
|
|
||||||
SettingsSectionView(
|
SettingsSectionView(showModal { UserProfileView(it) }, 60.dp) {
|
||||||
content = {
|
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.AccountCircle,
|
Icons.Outlined.AccountCircle,
|
||||||
contentDescription = "Avatar Placeholder",
|
contentDescription = "Avatar Placeholder",
|
||||||
tint = MaterialTheme.colors.onBackground,
|
|
||||||
)
|
)
|
||||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||||
Column {
|
Column {
|
||||||
@@ -69,136 +76,112 @@ fun SettingsLayout(
|
|||||||
profile.displayName,
|
profile.displayName,
|
||||||
style = MaterialTheme.typography.caption,
|
style = MaterialTheme.typography.caption,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colors.onBackground
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
profile.fullName,
|
|
||||||
color = MaterialTheme.colors.onBackground
|
|
||||||
)
|
)
|
||||||
|
Text(profile.fullName)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
func = { navigate(Pages.UserProfile.route) },
|
Divider(Modifier.padding(horizontal = 8.dp))
|
||||||
height = 60.dp
|
SettingsSectionView(showModal { UserAddressView(it) }) {
|
||||||
)
|
|
||||||
Divider(Modifier.padding(horizontal = 8.dp))
|
|
||||||
SettingsSectionView(
|
|
||||||
content = {
|
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.QrCode,
|
Icons.Outlined.QrCode,
|
||||||
contentDescription = "Address",
|
contentDescription = "Address",
|
||||||
tint = HighOrLowlight,
|
|
||||||
)
|
)
|
||||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||||
Text(
|
Text("Your SimpleX contact address")
|
||||||
"Your SimpleX contact address",
|
}
|
||||||
color = HighOrLowlight
|
Spacer(Modifier.height(24.dp))
|
||||||
)
|
|
||||||
},
|
|
||||||
func = { println("navigate to address") }
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(24.dp))
|
|
||||||
|
|
||||||
SettingsSectionView(
|
SettingsSectionView(showModal { HelpView(it) }) {
|
||||||
content = {
|
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.HelpOutline,
|
Icons.Outlined.HelpOutline,
|
||||||
contentDescription = "Help",
|
contentDescription = "Chat help",
|
||||||
tint = MaterialTheme.colors.onBackground,
|
|
||||||
)
|
)
|
||||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||||
Text(
|
Text("How to use SimpleX Chat")
|
||||||
"How to use SimpleX Chat",
|
}
|
||||||
color = MaterialTheme.colors.onBackground
|
Divider(Modifier.padding(horizontal = 8.dp))
|
||||||
|
SettingsSectionView(showModal { MarkdownHelpView() }) {
|
||||||
|
Icon(
|
||||||
|
Icons.Outlined.TextFormat,
|
||||||
|
contentDescription = "Markdown help",
|
||||||
)
|
)
|
||||||
},
|
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||||
func = { println("navigate to help") }
|
Text("Markdown in messages")
|
||||||
)
|
}
|
||||||
Divider(Modifier.padding(horizontal = 8.dp))
|
Divider(Modifier.padding(horizontal = 8.dp))
|
||||||
SettingsSectionView(
|
SettingsSectionView({ uriHandler.openUri(simplexTeamUri) }) {
|
||||||
content = {
|
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.Tag,
|
Icons.Outlined.Tag,
|
||||||
contentDescription = "SimpleX Team",
|
contentDescription = "SimpleX Team",
|
||||||
tint = MaterialTheme.colors.onBackground,
|
|
||||||
)
|
)
|
||||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||||
Text(
|
Text(
|
||||||
"Get help & advice via chat",
|
"Get help & advice via chat",
|
||||||
color = MaterialTheme.colors.primary
|
color = MaterialTheme.colors.primary
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
func = { uriHandler.openUri(simplexTeamUri) }
|
Divider(Modifier.padding(horizontal = 8.dp))
|
||||||
)
|
SettingsSectionView({ uriHandler.openUri("mailto:chat@simplex.chat") }) {
|
||||||
Divider(Modifier.padding(horizontal = 8.dp))
|
|
||||||
SettingsSectionView(
|
|
||||||
content = {
|
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.Email,
|
Icons.Outlined.Email,
|
||||||
contentDescription = "Email",
|
contentDescription = "Email",
|
||||||
tint = MaterialTheme.colors.onBackground,
|
|
||||||
)
|
)
|
||||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||||
Text(
|
Text(
|
||||||
"Ask questions via email",
|
"Ask questions via email",
|
||||||
color = MaterialTheme.colors.primary
|
color = MaterialTheme.colors.primary
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
func = { uriHandler.openUri("mailto:chat@simplex.chat") }
|
Spacer(Modifier.height(24.dp))
|
||||||
)
|
|
||||||
Spacer(Modifier.height(24.dp))
|
|
||||||
|
|
||||||
SettingsSectionView(
|
SettingsSectionView(showModal { SMPServersView(it) }) {
|
||||||
content = {
|
Icon(
|
||||||
|
Icons.Outlined.Dns,
|
||||||
|
contentDescription = "SMP servers",
|
||||||
|
)
|
||||||
|
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||||
|
Text("SMP servers")
|
||||||
|
}
|
||||||
|
Divider(Modifier.padding(horizontal = 8.dp))
|
||||||
|
SettingsSectionView(showTerminal) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.ic_outline_terminal),
|
painter = painterResource(id = R.drawable.ic_outline_terminal),
|
||||||
contentDescription = "Chat console",
|
contentDescription = "Chat console",
|
||||||
tint = MaterialTheme.colors.onBackground,
|
|
||||||
)
|
)
|
||||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||||
Text(
|
Text("Chat console")
|
||||||
"Chat console",
|
}
|
||||||
color = MaterialTheme.colors.onBackground
|
Divider(Modifier.padding(horizontal = 8.dp))
|
||||||
)
|
SettingsSectionView({ uriHandler.openUri("https://github.com/simplex-chat/simplex-chat") }) {
|
||||||
},
|
|
||||||
func = { navigate(Pages.Terminal.route) }
|
|
||||||
)
|
|
||||||
Divider(Modifier.padding(horizontal = 8.dp))
|
|
||||||
SettingsSectionView(
|
|
||||||
content = {
|
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.ic_github),
|
painter = painterResource(id = R.drawable.ic_github),
|
||||||
contentDescription = "GitHub",
|
contentDescription = "GitHub",
|
||||||
tint = MaterialTheme.colors.onBackground,
|
|
||||||
)
|
)
|
||||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||||
Text(
|
Text(
|
||||||
"Install ",
|
buildAnnotatedString {
|
||||||
color = MaterialTheme.colors.onBackground
|
append("Install ")
|
||||||
|
withStyle(SpanStyle(color = MaterialTheme.colors.primary)) {
|
||||||
|
append("SimpleX Chat for terminal")
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
Text(
|
}
|
||||||
"SimpleX Chat for terminal",
|
}
|
||||||
color = MaterialTheme.colors.primary
|
|
||||||
)
|
|
||||||
},
|
|
||||||
func = { uriHandler.openUri("https://github.com/simplex-chat/simplex-chat") }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsSectionView(content: (@Composable () -> Unit), func: () -> Unit, height: Dp = 48.dp) {
|
fun SettingsSectionView(click: () -> Unit, height: Dp = 48.dp, content: (@Composable () -> Unit)) {
|
||||||
Surface(
|
Row(
|
||||||
modifier = Modifier
|
Modifier
|
||||||
|
.padding(start = 8.dp)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(onClick = func)
|
.clickable(onClick = click)
|
||||||
.height(height),
|
.height(height),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Row(
|
content.invoke()
|
||||||
Modifier.padding(start = 8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
content.invoke()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +196,8 @@ fun PreviewSettingsLayout() {
|
|||||||
SimpleXTheme {
|
SimpleXTheme {
|
||||||
SettingsLayout(
|
SettingsLayout(
|
||||||
profile = Profile.sampleData,
|
profile = Profile.sampleData,
|
||||||
navigate = {}
|
showModal = {{}},
|
||||||
|
showTerminal = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package chat.simplex.app.views.usersettings
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
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.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import chat.simplex.app.model.ChatModel
|
||||||
|
import chat.simplex.app.ui.theme.SimpleButton
|
||||||
|
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||||
|
import chat.simplex.app.views.helpers.*
|
||||||
|
import chat.simplex.app.views.newchat.QRCode
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UserAddressView(chatModel: ChatModel) {
|
||||||
|
val cxt = LocalContext.current
|
||||||
|
UserAddressLayout(
|
||||||
|
userAddress = chatModel.userAddress.value,
|
||||||
|
createAddress = {
|
||||||
|
withApi {
|
||||||
|
chatModel.userAddress.value = chatModel.controller.apiCreateUserAddress()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
share = { userAddress: String -> shareText(cxt, userAddress) },
|
||||||
|
deleteAddress = {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
title = "Delete address?",
|
||||||
|
text = "All your contacts will remain connected",
|
||||||
|
confirmText = "Delete",
|
||||||
|
onConfirm = {
|
||||||
|
withApi {
|
||||||
|
chatModel.controller.apiDeleteUserAddress()
|
||||||
|
chatModel.userAddress.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UserAddressLayout(
|
||||||
|
userAddress: String?,
|
||||||
|
createAddress: () -> Unit,
|
||||||
|
share: (String) -> Unit,
|
||||||
|
deleteAddress: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.Top
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Your chat address",
|
||||||
|
Modifier.padding(bottom = 16.dp),
|
||||||
|
style = MaterialTheme.typography.h1,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"You can share your address as a link or as a QR code - anybody will be able to connect to you, " +
|
||||||
|
"and if you later delete it - you won't lose your contacts.",
|
||||||
|
Modifier.padding(bottom = 12.dp),
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.SpaceEvenly
|
||||||
|
) {
|
||||||
|
if (userAddress == null) {
|
||||||
|
SimpleButton("Create address", icon = Icons.Outlined.QrCode, click = createAddress)
|
||||||
|
} else {
|
||||||
|
QRCode(userAddress, Modifier.weight(1f, fill = false).aspectRatio(1f))
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(vertical = 10.dp)
|
||||||
|
) {
|
||||||
|
SimpleButton(
|
||||||
|
"Share link",
|
||||||
|
icon = Icons.Outlined.Share,
|
||||||
|
click = { share(userAddress) })
|
||||||
|
SimpleButton(
|
||||||
|
"Delete address",
|
||||||
|
icon = Icons.Outlined.Delete,
|
||||||
|
color = Color.Red,
|
||||||
|
click = deleteAddress
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Preview(
|
||||||
|
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||||
|
showBackground = true,
|
||||||
|
name = "Dark Mode"
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
fun PreviewUserAddressLayoutNoAddress() {
|
||||||
|
SimpleXTheme {
|
||||||
|
UserAddressLayout(
|
||||||
|
userAddress = null,
|
||||||
|
createAddress = {},
|
||||||
|
share = { _ -> },
|
||||||
|
deleteAddress = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Preview(
|
||||||
|
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||||
|
showBackground = true,
|
||||||
|
name = "Dark Mode"
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
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",
|
||||||
|
createAddress = {},
|
||||||
|
share = { _ -> },
|
||||||
|
deleteAddress = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package chat.simplex.app.views.usersettings
|
package chat.simplex.app.views.usersettings
|
||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
@@ -15,15 +14,13 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
|
||||||
import chat.simplex.app.model.ChatModel
|
import chat.simplex.app.model.ChatModel
|
||||||
import chat.simplex.app.model.Profile
|
import chat.simplex.app.model.Profile
|
||||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||||
import chat.simplex.app.views.helpers.CloseSheetBar
|
|
||||||
import chat.simplex.app.views.helpers.withApi
|
import chat.simplex.app.views.helpers.withApi
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UserProfileView(chatModel: ChatModel, nav: NavController) {
|
fun UserProfileView(chatModel: ChatModel) {
|
||||||
val user = chatModel.currentUser.value
|
val user = chatModel.currentUser.value
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
var editProfile by remember { mutableStateOf(false) }
|
var editProfile by remember { mutableStateOf(false) }
|
||||||
@@ -31,7 +28,6 @@ fun UserProfileView(chatModel: ChatModel, nav: NavController) {
|
|||||||
UserProfileLayout(
|
UserProfileLayout(
|
||||||
editProfile = editProfile,
|
editProfile = editProfile,
|
||||||
profile = profile,
|
profile = profile,
|
||||||
back = { nav.popBackStack() },
|
|
||||||
editProfileOff = { editProfile = false },
|
editProfileOff = { editProfile = false },
|
||||||
editProfileOn = { editProfile = true },
|
editProfileOn = { editProfile = true },
|
||||||
saveProfile = { displayName: String, fullName: String ->
|
saveProfile = { displayName: String, fullName: String ->
|
||||||
@@ -54,19 +50,11 @@ fun UserProfileView(chatModel: ChatModel, nav: NavController) {
|
|||||||
fun UserProfileLayout(
|
fun UserProfileLayout(
|
||||||
editProfile: Boolean,
|
editProfile: Boolean,
|
||||||
profile: Profile,
|
profile: Profile,
|
||||||
back: () -> Unit,
|
|
||||||
editProfileOff: () -> Unit,
|
editProfileOff: () -> Unit,
|
||||||
editProfileOn: () -> Unit,
|
editProfileOn: () -> Unit,
|
||||||
saveProfile: (String, String) -> Unit,
|
saveProfile: (String, String) -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(horizontalAlignment = Alignment.Start) {
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(MaterialTheme.colors.background)
|
|
||||||
.padding(horizontal = 8.dp),
|
|
||||||
horizontalAlignment = Alignment.Start
|
|
||||||
) {
|
|
||||||
CloseSheetBar(back)
|
|
||||||
Text(
|
Text(
|
||||||
"Your chat profile",
|
"Your chat profile",
|
||||||
Modifier.padding(bottom = 24.dp),
|
Modifier.padding(bottom = 24.dp),
|
||||||
@@ -179,14 +167,12 @@ fun UserProfileLayout(
|
|||||||
showBackground = true,
|
showBackground = true,
|
||||||
name = "Dark Mode"
|
name = "Dark Mode"
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PreviewUserProfileLayoutEditOff() {
|
fun PreviewUserProfileLayoutEditOff() {
|
||||||
SimpleXTheme {
|
SimpleXTheme {
|
||||||
UserProfileLayout(
|
UserProfileLayout(
|
||||||
profile = Profile.sampleData,
|
profile = Profile.sampleData,
|
||||||
editProfile = false,
|
editProfile = false,
|
||||||
back = {},
|
|
||||||
editProfileOff = {},
|
editProfileOff = {},
|
||||||
editProfileOn = {},
|
editProfileOn = {},
|
||||||
saveProfile = { _, _ -> }
|
saveProfile = { _, _ -> }
|
||||||
@@ -206,7 +192,6 @@ fun PreviewUserProfileLayoutEditOn() {
|
|||||||
UserProfileLayout(
|
UserProfileLayout(
|
||||||
profile = Profile.sampleData,
|
profile = Profile.sampleData,
|
||||||
editProfile = true,
|
editProfile = true,
|
||||||
back = {},
|
|
||||||
editProfileOff = {},
|
editProfileOff = {},
|
||||||
editProfileOn = {},
|
editProfileOn = {},
|
||||||
saveProfile = { _, _ -> }
|
saveProfile = { _, _ -> }
|
||||||
|
|||||||
|
After Width: | Height: | Size: 861 B |
|
After Width: | Height: | Size: 569 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/white"/>
|
||||||
|
<foreground android:drawable="@mipmap/icon_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/white"/>
|
||||||
|
<foreground android:drawable="@color/white"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 982 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 17 KiB |
@@ -1,10 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="purple_200">#FFBB86FC</color>
|
|
||||||
<color name="purple_500">#FF6200EE</color>
|
|
||||||
<color name="purple_700">#FF3700B3</color>
|
|
||||||
<color name="teal_200">#FF03DAC5</color>
|
|
||||||
<color name="teal_700">#FF018786</color>
|
|
||||||
<color name="black">#FF000000</color>
|
<color name="black">#FF000000</color>
|
||||||
<color name="white">#FFFFFFFF</color>
|
<color name="white">#FFFFFFFF</color>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="icon_background">#FFFFFF</color>
|
||||||
|
</resources>
|
||||||
@@ -2,6 +2,6 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="Theme.SimpleX" parent="android:Theme.Material.Light.NoActionBar">
|
<style name="Theme.SimpleX" parent="android:Theme.Material.Light.NoActionBar">
|
||||||
<item name="android:statusBarColor">@color/purple_700</item>
|
<item name="android:statusBarColor">@color/black</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -7,7 +7,7 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:7.1.1'
|
classpath 'com.android.tools.build:gradle:7.1.2'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"
|
||||||
classpath "org.jetbrains.kotlin:kotlin-serialization:1.3.2"
|
classpath "org.jetbrains.kotlin:kotlin-serialization:1.3.2"
|
||||||
|
|
||||||
@@ -16,8 +16,8 @@ buildscript {
|
|||||||
}
|
}
|
||||||
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application' version '7.1.1' apply false
|
id 'com.android.application' version '7.1.2' apply false
|
||||||
id 'com.android.library' version '7.1.1' apply false
|
id 'com.android.library' version '7.1.2' apply false
|
||||||
id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
|
id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
|
||||||
id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.10'
|
id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.10'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,3 +65,5 @@ fastlane/test_output
|
|||||||
iOSInjectionProject/
|
iOSInjectionProject/
|
||||||
|
|
||||||
Libraries/
|
Libraries/
|
||||||
|
|
||||||
|
Shared/MyPlayground.playground/*
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ struct ContentView: View {
|
|||||||
.onAppear {
|
.onAppear {
|
||||||
do {
|
do {
|
||||||
try apiStartChat()
|
try apiStartChat()
|
||||||
|
chatModel.userSMPServers = try getUserSMPServers()
|
||||||
chatModel.chats = try apiGetChats()
|
chatModel.chats = try apiGetChats()
|
||||||
} catch {
|
} catch {
|
||||||
fatalError("Failed to start or load chats: \(error)")
|
fatalError("Failed to start or load chats: \(error)")
|
||||||
@@ -36,7 +37,7 @@ struct ContentView: View {
|
|||||||
func notificationAlert() -> Alert {
|
func notificationAlert() -> Alert {
|
||||||
Alert(
|
Alert(
|
||||||
title: Text("Notification are disabled!"),
|
title: Text("Notification are disabled!"),
|
||||||
message: Text("Please open settings to enable"),
|
message: Text("The app can notify you when you receive messages or contact requests - please open settings to enable."),
|
||||||
primaryButton: .default(Text("Open Settings")) {
|
primaryButton: .default(Text("Open Settings")) {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
|
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ final class ChatModel: ObservableObject {
|
|||||||
// items in the terminal view
|
// items in the terminal view
|
||||||
@Published var terminalItems: [TerminalItem] = []
|
@Published var terminalItems: [TerminalItem] = []
|
||||||
@Published var userAddress: String?
|
@Published var userAddress: String?
|
||||||
|
@Published var userSMPServers: [String]?
|
||||||
@Published var appOpenUrl: URL?
|
@Published var appOpenUrl: URL?
|
||||||
|
|
||||||
|
var messageDelivery: Dictionary<Int64, () -> Void> = [:]
|
||||||
|
|
||||||
static let shared = ChatModel()
|
static let shared = ChatModel()
|
||||||
|
|
||||||
func hasChat(_ id: String) -> Bool {
|
func hasChat(_ id: String) -> Bool {
|
||||||
@@ -97,7 +101,7 @@ final class ChatModel: ObservableObject {
|
|||||||
if case .rcvNew = cItem.meta.itemStatus {
|
if case .rcvNew = cItem.meta.itemStatus {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||||
if self.chatId == cInfo.id {
|
if self.chatId == cInfo.id {
|
||||||
SimpleX.markChatItemRead(cInfo, cItem)
|
Task { await SimpleX.markChatItemRead(cInfo, cItem) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -426,6 +430,11 @@ struct Contact: Identifiable, Decodable, NamedChat {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ContactSubStatus: Decodable {
|
||||||
|
var contact: Contact
|
||||||
|
var contactError: ChatError?
|
||||||
|
}
|
||||||
|
|
||||||
struct Connection: Decodable {
|
struct Connection: Decodable {
|
||||||
var connStatus: String
|
var connStatus: String
|
||||||
|
|
||||||
@@ -503,6 +512,11 @@ struct GroupMember: Decodable {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct MemberSubError: Decodable {
|
||||||
|
var member: GroupMember
|
||||||
|
var memberError: ChatError
|
||||||
|
}
|
||||||
|
|
||||||
struct AChatItem: Decodable {
|
struct AChatItem: Decodable {
|
||||||
var chatInfo: ChatInfo
|
var chatInfo: ChatInfo
|
||||||
var chatItem: ChatItem
|
var chatItem: ChatItem
|
||||||
@@ -512,10 +526,11 @@ struct ChatItem: Identifiable, Decodable {
|
|||||||
var chatDir: CIDirection
|
var chatDir: CIDirection
|
||||||
var meta: CIMeta
|
var meta: CIMeta
|
||||||
var content: CIContent
|
var content: CIContent
|
||||||
|
var formattedText: [FormattedText]?
|
||||||
|
|
||||||
var id: Int64 { get { meta.itemId } }
|
var id: Int64 { get { meta.itemId } }
|
||||||
|
|
||||||
var timestampText: String { get { meta.timestampText } }
|
var timestampText: Text { get { meta.timestampText } }
|
||||||
|
|
||||||
func isRcvNew() -> Bool {
|
func isRcvNew() -> Bool {
|
||||||
if case .rcvNew = meta.itemStatus { return true }
|
if case .rcvNew = meta.itemStatus { return true }
|
||||||
@@ -556,7 +571,7 @@ struct CIMeta: Decodable {
|
|||||||
var itemStatus: CIStatus
|
var itemStatus: CIStatus
|
||||||
var createdAt: Date
|
var createdAt: Date
|
||||||
|
|
||||||
var timestampText: String { get { SimpleX.timestampText(itemTs) } }
|
var timestampText: Text { get { SimpleX.timestampText(itemTs) } }
|
||||||
|
|
||||||
static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew) -> CIMeta {
|
static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew) -> CIMeta {
|
||||||
CIMeta(
|
CIMeta(
|
||||||
@@ -569,13 +584,14 @@ struct CIMeta: Decodable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let msgTimeFormat = Date.FormatStyle.dateTime.hour().minute()
|
||||||
|
let msgDateFormat = Date.FormatStyle.dateTime.day(.twoDigits).month(.twoDigits)
|
||||||
|
|
||||||
func timestampText(_ date: Date) -> String {
|
func timestampText(_ date: Date) -> Text {
|
||||||
let now = Calendar.current.dateComponents([.day, .hour], from: .now)
|
let now = Calendar.current.dateComponents([.day, .hour], from: .now)
|
||||||
let dc = Calendar.current.dateComponents([.day, .hour], from: date)
|
let dc = Calendar.current.dateComponents([.day, .hour], from: date)
|
||||||
return now.day == dc.day || ((now.day ?? 0) - (dc.day ?? 0) == 1 && (dc.hour ?? 0) >= 18 && (now.hour ?? 0) < 12)
|
let recent = now.day == dc.day || ((now.day ?? 0) - (dc.day ?? 0) == 1 && (dc.hour ?? 0) >= 18 && (now.hour ?? 0) < 12)
|
||||||
? date.formatted(date: .omitted, time: .shortened)
|
return Text(date, format: recent ? msgTimeFormat : msgDateFormat)
|
||||||
: String(date.formatted(date: .numeric, time: .omitted).prefix(5))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CIStatus: Decodable {
|
enum CIStatus: Decodable {
|
||||||
@@ -611,15 +627,14 @@ struct RcvFileTransfer: Decodable {
|
|||||||
|
|
||||||
enum MsgContent {
|
enum MsgContent {
|
||||||
case text(String)
|
case text(String)
|
||||||
|
// TODO include original JSON, possibly using https://github.com/zoul/generic-json-swift
|
||||||
case unknown(type: String, text: String)
|
case unknown(type: String, text: String)
|
||||||
case invalid(error: String)
|
|
||||||
|
|
||||||
var text: String {
|
var text: String {
|
||||||
get {
|
get {
|
||||||
switch self {
|
switch self {
|
||||||
case let .text(text): return text
|
case let .text(text): return text
|
||||||
case let .unknown(_, text): return text
|
case let .unknown(_, text): return text
|
||||||
case .invalid: return "invalid"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -641,8 +656,8 @@ enum MsgContent {
|
|||||||
|
|
||||||
extension MsgContent: Decodable {
|
extension MsgContent: Decodable {
|
||||||
init(from decoder: Decoder) throws {
|
init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
do {
|
do {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
let type = try container.decode(String.self, forKey: CodingKeys.type)
|
let type = try container.decode(String.self, forKey: CodingKeys.type)
|
||||||
switch type {
|
switch type {
|
||||||
case "text":
|
case "text":
|
||||||
@@ -653,7 +668,50 @@ extension MsgContent: Decodable {
|
|||||||
self = .unknown(type: type, text: text ?? "unknown message format")
|
self = .unknown(type: type, text: text ?? "unknown message format")
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
self = .invalid(error: String(describing: error))
|
self = .unknown(type: "unknown", text: "invalid message format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FormattedText: Decodable {
|
||||||
|
var text: String
|
||||||
|
var format: Format?
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Format: Decodable {
|
||||||
|
case bold
|
||||||
|
case italic
|
||||||
|
case strikeThrough
|
||||||
|
case snippet
|
||||||
|
case secret
|
||||||
|
case colored(color: FormatColor)
|
||||||
|
case uri
|
||||||
|
case email
|
||||||
|
case phone
|
||||||
|
}
|
||||||
|
|
||||||
|
enum FormatColor: String, Decodable {
|
||||||
|
case red = "red"
|
||||||
|
case green = "green"
|
||||||
|
case blue = "blue"
|
||||||
|
case yellow = "yellow"
|
||||||
|
case cyan = "cyan"
|
||||||
|
case magenta = "magenta"
|
||||||
|
case black = "black"
|
||||||
|
case white = "white"
|
||||||
|
|
||||||
|
var uiColor: Color {
|
||||||
|
get {
|
||||||
|
switch (self) {
|
||||||
|
case .red: return .red
|
||||||
|
case .green: return .green
|
||||||
|
case .blue: return .blue
|
||||||
|
case .yellow: return .yellow
|
||||||
|
case .cyan: return .cyan
|
||||||
|
case .magenta: return .purple
|
||||||
|
case .black: return .primary
|
||||||
|
case .white: return .primary
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
|||||||
if content.categoryIdentifier == ntfCategoryContactRequest && response.actionIdentifier == ntfActionAccept,
|
if content.categoryIdentifier == ntfCategoryContactRequest && response.actionIdentifier == ntfActionAccept,
|
||||||
let chatId = content.userInfo["chatId"] as? String,
|
let chatId = content.userInfo["chatId"] as? String,
|
||||||
case let .contactRequest(contactRequest) = chatModel.getChat(chatId)?.chatInfo {
|
case let .contactRequest(contactRequest) = chatModel.getChat(chatId)?.chatInfo {
|
||||||
acceptContactRequest(contactRequest)
|
Task { await acceptContactRequest(contactRequest) }
|
||||||
} else {
|
} else {
|
||||||
chatModel.chatId = content.targetContentIdentifier
|
chatModel.chatId = content.targetContentIdentifier
|
||||||
}
|
}
|
||||||
@@ -178,6 +178,7 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
|||||||
if let s = body { content.body = s }
|
if let s = body { content.body = s }
|
||||||
content.targetContentIdentifier = targetContentIdentifier
|
content.targetContentIdentifier = targetContentIdentifier
|
||||||
content.userInfo = userInfo
|
content.userInfo = userInfo
|
||||||
|
// TODO move logic of adding sound here, so it applies to background notifications too
|
||||||
content.sound = .default
|
content.sound = .default
|
||||||
// content.interruptionLevel = .active
|
// content.interruptionLevel = .active
|
||||||
// content.relevanceScore = 0.5 // 0-1
|
// content.relevanceScore = 0.5 // 0-1
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ enum ChatCommand {
|
|||||||
case apiGetChats
|
case apiGetChats
|
||||||
case apiGetChat(type: ChatType, id: Int64)
|
case apiGetChat(type: ChatType, id: Int64)
|
||||||
case apiSendMessage(type: ChatType, id: Int64, msg: MsgContent)
|
case apiSendMessage(type: ChatType, id: Int64, msg: MsgContent)
|
||||||
|
case getUserSMPServers
|
||||||
|
case setUserSMPServers(smpServers: [String])
|
||||||
case addContact
|
case addContact
|
||||||
case connect(connReq: String)
|
case connect(connReq: String)
|
||||||
case apiDeleteChat(type: ChatType, id: Int64)
|
case apiDeleteChat(type: ChatType, id: Int64)
|
||||||
@@ -43,6 +45,8 @@ enum ChatCommand {
|
|||||||
case .apiGetChats: return "/_get chats"
|
case .apiGetChats: return "/_get chats"
|
||||||
case let .apiGetChat(type, id): return "/_get chat \(ref(type, id)) count=100"
|
case let .apiGetChat(type, id): return "/_get chat \(ref(type, id)) count=100"
|
||||||
case let .apiSendMessage(type, id, mc): return "/_send \(ref(type, id)) \(mc.cmdString)"
|
case let .apiSendMessage(type, id, mc): return "/_send \(ref(type, id)) \(mc.cmdString)"
|
||||||
|
case .getUserSMPServers: return "/smp_servers"
|
||||||
|
case let .setUserSMPServers(smpServers): return "/smp_servers \(smpServersStr(smpServers: smpServers))"
|
||||||
case .addContact: return "/connect"
|
case .addContact: return "/connect"
|
||||||
case let .connect(connReq): return "/connect \(connReq)"
|
case let .connect(connReq): return "/connect \(connReq)"
|
||||||
case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))"
|
case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))"
|
||||||
@@ -67,6 +71,8 @@ enum ChatCommand {
|
|||||||
case .apiGetChats: return "apiGetChats"
|
case .apiGetChats: return "apiGetChats"
|
||||||
case .apiGetChat: return "apiGetChat"
|
case .apiGetChat: return "apiGetChat"
|
||||||
case .apiSendMessage: return "apiSendMessage"
|
case .apiSendMessage: return "apiSendMessage"
|
||||||
|
case .getUserSMPServers: return "getUserSMPServers"
|
||||||
|
case .setUserSMPServers: return "setUserSMPServers"
|
||||||
case .addContact: return "addContact"
|
case .addContact: return "addContact"
|
||||||
case .connect: return "connect"
|
case .connect: return "connect"
|
||||||
case .apiDeleteChat: return "apiDeleteChat"
|
case .apiDeleteChat: return "apiDeleteChat"
|
||||||
@@ -85,6 +91,10 @@ enum ChatCommand {
|
|||||||
func ref(_ type: ChatType, _ id: Int64) -> String {
|
func ref(_ type: ChatType, _ id: Int64) -> String {
|
||||||
"\(type.rawValue)\(id)"
|
"\(type.rawValue)\(id)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func smpServersStr(smpServers: [String]) -> String {
|
||||||
|
smpServers.isEmpty ? "default" : smpServers.joined(separator: ",")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct APIResponse: Decodable {
|
struct APIResponse: Decodable {
|
||||||
@@ -95,8 +105,10 @@ enum ChatResponse: Decodable, Error {
|
|||||||
case response(type: String, json: String)
|
case response(type: String, json: String)
|
||||||
case activeUser(user: User)
|
case activeUser(user: User)
|
||||||
case chatStarted
|
case chatStarted
|
||||||
|
case chatRunning
|
||||||
case apiChats(chats: [ChatData])
|
case apiChats(chats: [ChatData])
|
||||||
case apiChat(chat: ChatData)
|
case apiChat(chat: ChatData)
|
||||||
|
case userSMPServers(smpServers: [String])
|
||||||
case invitation(connReqInvitation: String)
|
case invitation(connReqInvitation: String)
|
||||||
case sentConfirmation
|
case sentConfirmation
|
||||||
case sentInvitation
|
case sentInvitation
|
||||||
@@ -114,7 +126,9 @@ enum ChatResponse: Decodable, Error {
|
|||||||
case contactSubscribed(contact: Contact)
|
case contactSubscribed(contact: Contact)
|
||||||
case contactDisconnected(contact: Contact)
|
case contactDisconnected(contact: Contact)
|
||||||
case contactSubError(contact: Contact, chatError: ChatError)
|
case contactSubError(contact: Contact, chatError: ChatError)
|
||||||
|
case contactSubSummary(contactSubscriptions: [ContactSubStatus])
|
||||||
case groupSubscribed(groupInfo: GroupInfo)
|
case groupSubscribed(groupInfo: GroupInfo)
|
||||||
|
case memberSubErrors(memberSubErrors: [MemberSubError])
|
||||||
case groupEmpty(groupInfo: GroupInfo)
|
case groupEmpty(groupInfo: GroupInfo)
|
||||||
case userContactLinkSubscribed
|
case userContactLinkSubscribed
|
||||||
case newChatItem(chatItem: AChatItem)
|
case newChatItem(chatItem: AChatItem)
|
||||||
@@ -129,8 +143,10 @@ enum ChatResponse: Decodable, Error {
|
|||||||
case let .response(type, _): return "* \(type)"
|
case let .response(type, _): return "* \(type)"
|
||||||
case .activeUser: return "activeUser"
|
case .activeUser: return "activeUser"
|
||||||
case .chatStarted: return "chatStarted"
|
case .chatStarted: return "chatStarted"
|
||||||
|
case .chatRunning: return "chatRunning"
|
||||||
case .apiChats: return "apiChats"
|
case .apiChats: return "apiChats"
|
||||||
case .apiChat: return "apiChat"
|
case .apiChat: return "apiChat"
|
||||||
|
case .userSMPServers: return "userSMPServers"
|
||||||
case .invitation: return "invitation"
|
case .invitation: return "invitation"
|
||||||
case .sentConfirmation: return "sentConfirmation"
|
case .sentConfirmation: return "sentConfirmation"
|
||||||
case .sentInvitation: return "sentInvitation"
|
case .sentInvitation: return "sentInvitation"
|
||||||
@@ -148,7 +164,9 @@ enum ChatResponse: Decodable, Error {
|
|||||||
case .contactSubscribed: return "contactSubscribed"
|
case .contactSubscribed: return "contactSubscribed"
|
||||||
case .contactDisconnected: return "contactDisconnected"
|
case .contactDisconnected: return "contactDisconnected"
|
||||||
case .contactSubError: return "contactSubError"
|
case .contactSubError: return "contactSubError"
|
||||||
|
case .contactSubSummary: return "contactSubSummary"
|
||||||
case .groupSubscribed: return "groupSubscribed"
|
case .groupSubscribed: return "groupSubscribed"
|
||||||
|
case .memberSubErrors: return "memberSubErrors"
|
||||||
case .groupEmpty: return "groupEmpty"
|
case .groupEmpty: return "groupEmpty"
|
||||||
case .userContactLinkSubscribed: return "userContactLinkSubscribed"
|
case .userContactLinkSubscribed: return "userContactLinkSubscribed"
|
||||||
case .newChatItem: return "newChatItem"
|
case .newChatItem: return "newChatItem"
|
||||||
@@ -166,8 +184,10 @@ enum ChatResponse: Decodable, Error {
|
|||||||
case let .response(_, json): return json
|
case let .response(_, json): return json
|
||||||
case let .activeUser(user): return String(describing: user)
|
case let .activeUser(user): return String(describing: user)
|
||||||
case .chatStarted: return noDetails
|
case .chatStarted: return noDetails
|
||||||
|
case .chatRunning: return noDetails
|
||||||
case let .apiChats(chats): return String(describing: chats)
|
case let .apiChats(chats): return String(describing: chats)
|
||||||
case let .apiChat(chat): return String(describing: chat)
|
case let .apiChat(chat): return String(describing: chat)
|
||||||
|
case let .userSMPServers(smpServers): return String(describing: smpServers)
|
||||||
case let .invitation(connReqInvitation): return connReqInvitation
|
case let .invitation(connReqInvitation): return connReqInvitation
|
||||||
case .sentConfirmation: return noDetails
|
case .sentConfirmation: return noDetails
|
||||||
case .sentInvitation: return noDetails
|
case .sentInvitation: return noDetails
|
||||||
@@ -185,7 +205,9 @@ enum ChatResponse: Decodable, Error {
|
|||||||
case let .contactSubscribed(contact): return String(describing: contact)
|
case let .contactSubscribed(contact): return String(describing: contact)
|
||||||
case let .contactDisconnected(contact): return String(describing: contact)
|
case let .contactDisconnected(contact): return String(describing: contact)
|
||||||
case let .contactSubError(contact, chatError): return "contact:\n\(String(describing: contact))\nerror:\n\(String(describing: chatError))"
|
case let .contactSubError(contact, chatError): return "contact:\n\(String(describing: contact))\nerror:\n\(String(describing: chatError))"
|
||||||
|
case let .contactSubSummary(contactSubscriptions): return String(describing: contactSubscriptions)
|
||||||
case let .groupSubscribed(groupInfo): return String(describing: groupInfo)
|
case let .groupSubscribed(groupInfo): return String(describing: groupInfo)
|
||||||
|
case let .memberSubErrors(memberSubErrors): return String(describing: memberSubErrors)
|
||||||
case let .groupEmpty(groupInfo): return String(describing: groupInfo)
|
case let .groupEmpty(groupInfo): return String(describing: groupInfo)
|
||||||
case .userContactLinkSubscribed: return noDetails
|
case .userContactLinkSubscribed: return noDetails
|
||||||
case let .newChatItem(chatItem): return String(describing: chatItem)
|
case let .newChatItem(chatItem): return String(describing: chatItem)
|
||||||
@@ -232,10 +254,53 @@ enum TerminalItem: Identifiable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func chatSendCmd(_ cmd: ChatCommand) throws -> ChatResponse {
|
private func _sendCmd(_ cmd: ChatCommand) -> ChatResponse {
|
||||||
var c = cmd.cmdString.cString(using: .utf8)!
|
var c = cmd.cmdString.cString(using: .utf8)!
|
||||||
|
return chatResponse(chat_send_cmd(getChatCtrl(), &c))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func beginBGTask(_ handler: (() -> Void)? = nil) -> (() -> Void) {
|
||||||
|
var id: UIBackgroundTaskIdentifier!
|
||||||
|
var running = true
|
||||||
|
let endTask = {
|
||||||
|
// logger.debug("beginBGTask: endTask \(id.rawValue)")
|
||||||
|
if running {
|
||||||
|
running = false
|
||||||
|
if let h = handler {
|
||||||
|
// logger.debug("beginBGTask: user handler")
|
||||||
|
h()
|
||||||
|
}
|
||||||
|
if id != .invalid {
|
||||||
|
UIApplication.shared.endBackgroundTask(id)
|
||||||
|
id = .invalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
id = UIApplication.shared.beginBackgroundTask(expirationHandler: endTask)
|
||||||
|
// logger.debug("beginBGTask: \(id.rawValue)")
|
||||||
|
return endTask
|
||||||
|
}
|
||||||
|
|
||||||
|
let msgDelay: Double = 7.5
|
||||||
|
let maxTaskDuration: Double = 15
|
||||||
|
|
||||||
|
private func withBGTask(bgDelay: Double? = nil, f: @escaping () -> ChatResponse) -> ChatResponse {
|
||||||
|
let endTask = beginBGTask()
|
||||||
|
DispatchQueue.global().asyncAfter(deadline: .now() + maxTaskDuration, execute: endTask)
|
||||||
|
let r = f()
|
||||||
|
if let d = bgDelay {
|
||||||
|
DispatchQueue.global().asyncAfter(deadline: .now() + d, execute: endTask)
|
||||||
|
} else {
|
||||||
|
endTask()
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil) -> ChatResponse {
|
||||||
logger.debug("chatSendCmd \(cmd.cmdType)")
|
logger.debug("chatSendCmd \(cmd.cmdType)")
|
||||||
let resp = chatResponse(chat_send_cmd(getChatCtrl(), &c)!)
|
let resp = bgTask
|
||||||
|
? withBGTask(bgDelay: bgDelay) { _sendCmd(cmd) }
|
||||||
|
: _sendCmd(cmd)
|
||||||
logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)")
|
logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)")
|
||||||
if case let .response(_, json) = resp {
|
if case let .response(_, json) = resp {
|
||||||
logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)")
|
logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)")
|
||||||
@@ -247,13 +312,25 @@ func chatSendCmd(_ cmd: ChatCommand) throws -> ChatResponse {
|
|||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
func chatRecvMsg() throws -> ChatResponse {
|
func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil) async -> ChatResponse {
|
||||||
chatResponse(chat_recv_msg(getChatCtrl())!)
|
await withCheckedContinuation { cont in
|
||||||
|
cont.resume(returning: chatSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func chatRecvMsg() async -> ChatResponse {
|
||||||
|
await withCheckedContinuation { cont in
|
||||||
|
_ = withBGTask(bgDelay: msgDelay) {
|
||||||
|
let resp = chatResponse(chat_recv_msg(getChatCtrl())!)
|
||||||
|
cont.resume(returning: resp)
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiGetActiveUser() throws -> User? {
|
func apiGetActiveUser() throws -> User? {
|
||||||
let _ = getChatCtrl()
|
let _ = getChatCtrl()
|
||||||
let r = try chatSendCmd(.showActiveUser)
|
let r = chatSendCmdSync(.showActiveUser)
|
||||||
switch r {
|
switch r {
|
||||||
case let .activeUser(user): return user
|
case let .activeUser(user): return user
|
||||||
case .chatCmdError(.error(.noActiveUser)): return nil
|
case .chatCmdError(.error(.noActiveUser)): return nil
|
||||||
@@ -262,43 +339,72 @@ func apiGetActiveUser() throws -> User? {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func apiCreateActiveUser(_ p: Profile) throws -> User {
|
func apiCreateActiveUser(_ p: Profile) throws -> User {
|
||||||
let r = try chatSendCmd(.createActiveUser(profile: p))
|
let r = chatSendCmdSync(.createActiveUser(profile: p))
|
||||||
if case let .activeUser(user) = r { return user }
|
if case let .activeUser(user) = r { return user }
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiStartChat() throws {
|
func apiStartChat() throws {
|
||||||
let r = try chatSendCmd(.startChat)
|
let r = chatSendCmdSync(.startChat)
|
||||||
if case .chatStarted = r { return }
|
if case .chatStarted = r { return }
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiGetChats() throws -> [Chat] {
|
func apiGetChats() throws -> [Chat] {
|
||||||
let r = try chatSendCmd(.apiGetChats)
|
let r = chatSendCmdSync(.apiGetChats)
|
||||||
if case let .apiChats(chats) = r { return chats.map { Chat.init($0) } }
|
if case let .apiChats(chats) = r { return chats.map { Chat.init($0) } }
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiGetChat(type: ChatType, id: Int64) throws -> Chat {
|
func apiGetChat(type: ChatType, id: Int64) async throws -> Chat {
|
||||||
let r = try chatSendCmd(.apiGetChat(type: type, id: id))
|
let r = await chatSendCmd(.apiGetChat(type: type, id: id))
|
||||||
if case let .apiChat(chat) = r { return Chat.init(chat) }
|
if case let .apiChat(chat) = r { return Chat.init(chat) }
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiSendMessage(type: ChatType, id: Int64, msg: MsgContent) throws -> ChatItem {
|
func apiSendMessage(type: ChatType, id: Int64, msg: MsgContent) async throws -> ChatItem {
|
||||||
let r = try chatSendCmd(.apiSendMessage(type: type, id: id, msg: msg))
|
let chatModel = ChatModel.shared
|
||||||
if case let .newChatItem(aChatItem) = r { return aChatItem.chatItem }
|
let cmd = ChatCommand.apiSendMessage(type: type, id: id, msg: msg)
|
||||||
|
let r: ChatResponse
|
||||||
|
if type == .direct {
|
||||||
|
var cItem: ChatItem!
|
||||||
|
let endTask = beginBGTask({ if cItem != nil { chatModel.messageDelivery.removeValue(forKey: cItem.id) } })
|
||||||
|
r = await chatSendCmd(cmd, bgTask: false)
|
||||||
|
if case let .newChatItem(aChatItem) = r {
|
||||||
|
cItem = aChatItem.chatItem
|
||||||
|
chatModel.messageDelivery[cItem.id] = endTask
|
||||||
|
return cItem
|
||||||
|
}
|
||||||
|
endTask()
|
||||||
|
} else {
|
||||||
|
r = await chatSendCmd(cmd, bgDelay: msgDelay)
|
||||||
|
if case let .newChatItem(aChatItem) = r {
|
||||||
|
return aChatItem.chatItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw r
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserSMPServers() throws -> [String] {
|
||||||
|
let r = chatSendCmdSync(.getUserSMPServers)
|
||||||
|
if case let .userSMPServers(smpServers) = r { return smpServers }
|
||||||
|
throw r
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUserSMPServers(smpServers: [String]) async throws {
|
||||||
|
let r = await chatSendCmd(.setUserSMPServers(smpServers: smpServers))
|
||||||
|
if case .cmdOk = r { return }
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiAddContact() throws -> String {
|
func apiAddContact() throws -> String {
|
||||||
let r = try chatSendCmd(.addContact)
|
let r = chatSendCmdSync(.addContact, bgTask: false)
|
||||||
if case let .invitation(connReqInvitation) = r { return connReqInvitation }
|
if case let .invitation(connReqInvitation) = r { return connReqInvitation }
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiConnect(connReq: String) throws {
|
func apiConnect(connReq: String) async throws {
|
||||||
let r = try chatSendCmd(.connect(connReq: connReq))
|
let r = await chatSendCmd(.connect(connReq: connReq))
|
||||||
switch r {
|
switch r {
|
||||||
case .sentConfirmation: return
|
case .sentConfirmation: return
|
||||||
case .sentInvitation: return
|
case .sentInvitation: return
|
||||||
@@ -306,14 +412,14 @@ func apiConnect(connReq: String) throws {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiDeleteChat(type: ChatType, id: Int64) throws {
|
func apiDeleteChat(type: ChatType, id: Int64) async throws {
|
||||||
let r = try chatSendCmd(.apiDeleteChat(type: type, id: id))
|
let r = await chatSendCmd(.apiDeleteChat(type: type, id: id), bgTask: false)
|
||||||
if case .contactDeleted = r { return }
|
if case .contactDeleted = r { return }
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiUpdateProfile(profile: Profile) throws -> Profile? {
|
func apiUpdateProfile(profile: Profile) async throws -> Profile? {
|
||||||
let r = try chatSendCmd(.updateProfile(profile: profile))
|
let r = await chatSendCmd(.updateProfile(profile: profile))
|
||||||
switch r {
|
switch r {
|
||||||
case .userProfileNoChange: return nil
|
case .userProfileNoChange: return nil
|
||||||
case let .userProfileUpdated(_, toProfile): return toProfile
|
case let .userProfileUpdated(_, toProfile): return toProfile
|
||||||
@@ -321,20 +427,20 @@ func apiUpdateProfile(profile: Profile) throws -> Profile? {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiCreateUserAddress() throws -> String {
|
func apiCreateUserAddress() async throws -> String {
|
||||||
let r = try chatSendCmd(.createMyAddress)
|
let r = await chatSendCmd(.createMyAddress)
|
||||||
if case let .userContactLinkCreated(connReq) = r { return connReq }
|
if case let .userContactLinkCreated(connReq) = r { return connReq }
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiDeleteUserAddress() throws {
|
func apiDeleteUserAddress() async throws {
|
||||||
let r = try chatSendCmd(.deleteMyAddress)
|
let r = await chatSendCmd(.deleteMyAddress)
|
||||||
if case .userContactLinkDeleted = r { return }
|
if case .userContactLinkDeleted = r { return }
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiGetUserAddress() throws -> String? {
|
func apiGetUserAddress() async throws -> String? {
|
||||||
let r = try chatSendCmd(.showMyAddress)
|
let r = await chatSendCmd(.showMyAddress)
|
||||||
switch r {
|
switch r {
|
||||||
case let .userContactLink(connReq):
|
case let .userContactLink(connReq):
|
||||||
return connReq
|
return connReq
|
||||||
@@ -344,59 +450,59 @@ func apiGetUserAddress() throws -> String? {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiAcceptContactRequest(contactReqId: Int64) throws -> Contact {
|
func apiAcceptContactRequest(contactReqId: Int64) async throws -> Contact {
|
||||||
let r = try chatSendCmd(.apiAcceptContact(contactReqId: contactReqId))
|
let r = await chatSendCmd(.apiAcceptContact(contactReqId: contactReqId))
|
||||||
if case let .acceptingContactRequest(contact) = r { return contact }
|
if case let .acceptingContactRequest(contact) = r { return contact }
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiRejectContactRequest(contactReqId: Int64) throws {
|
func apiRejectContactRequest(contactReqId: Int64) async throws {
|
||||||
let r = try chatSendCmd(.apiRejectContact(contactReqId: contactReqId))
|
let r = await chatSendCmd(.apiRejectContact(contactReqId: contactReqId))
|
||||||
if case .contactRequestRejected = r { return }
|
if case .contactRequestRejected = r { return }
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) throws {
|
func apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) async throws {
|
||||||
let r = try chatSendCmd(.apiChatRead(type: type, id: id, itemRange: itemRange))
|
let r = await chatSendCmd(.apiChatRead(type: type, id: id, itemRange: itemRange))
|
||||||
if case .cmdOk = r { return }
|
if case .cmdOk = r { return }
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func acceptContactRequest(_ contactRequest: UserContactRequest) {
|
func acceptContactRequest(_ contactRequest: UserContactRequest) async {
|
||||||
do {
|
do {
|
||||||
let contact = try apiAcceptContactRequest(contactReqId: contactRequest.apiId)
|
let contact = try await apiAcceptContactRequest(contactReqId: contactRequest.apiId)
|
||||||
let chat = Chat(chatInfo: ChatInfo.direct(contact: contact), chatItems: [])
|
let chat = Chat(chatInfo: ChatInfo.direct(contact: contact), chatItems: [])
|
||||||
ChatModel.shared.replaceChat(contactRequest.id, chat)
|
DispatchQueue.main.async { ChatModel.shared.replaceChat(contactRequest.id, chat) }
|
||||||
} catch let error {
|
} catch let error {
|
||||||
logger.error("acceptContactRequest error: \(error.localizedDescription)")
|
logger.error("acceptContactRequest error: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func rejectContactRequest(_ contactRequest: UserContactRequest) {
|
func rejectContactRequest(_ contactRequest: UserContactRequest) async {
|
||||||
do {
|
do {
|
||||||
try apiRejectContactRequest(contactReqId: contactRequest.apiId)
|
try await apiRejectContactRequest(contactReqId: contactRequest.apiId)
|
||||||
ChatModel.shared.removeChat(contactRequest.id)
|
DispatchQueue.main.async { ChatModel.shared.removeChat(contactRequest.id) }
|
||||||
} catch let error {
|
} catch let error {
|
||||||
logger.error("rejectContactRequest: \(error.localizedDescription)")
|
logger.error("rejectContactRequest: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func markChatRead(_ chat: Chat) {
|
func markChatRead(_ chat: Chat) async {
|
||||||
do {
|
do {
|
||||||
let minItemId = chat.chatStats.minUnreadItemId
|
let minItemId = chat.chatStats.minUnreadItemId
|
||||||
let itemRange = (minItemId, chat.chatItems.last?.id ?? minItemId)
|
let itemRange = (minItemId, chat.chatItems.last?.id ?? minItemId)
|
||||||
let cInfo = chat.chatInfo
|
let cInfo = chat.chatInfo
|
||||||
try apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: itemRange)
|
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: itemRange)
|
||||||
ChatModel.shared.markChatItemsRead(cInfo)
|
DispatchQueue.main.async { ChatModel.shared.markChatItemsRead(cInfo) }
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("markChatRead apiChatRead error: \(error.localizedDescription)")
|
logger.error("markChatRead apiChatRead error: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) {
|
func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async {
|
||||||
do {
|
do {
|
||||||
try apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: (cItem.id, cItem.id))
|
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: (cItem.id, cItem.id))
|
||||||
ChatModel.shared.markChatItemRead(cInfo, cItem)
|
DispatchQueue.main.async { ChatModel.shared.markChatItemRead(cInfo, cItem) }
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("markChatItemRead apiChatRead error: \(error.localizedDescription)")
|
logger.error("markChatItemRead apiChatRead error: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
@@ -411,7 +517,7 @@ func initializeChat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ChatReceiver {
|
class ChatReceiver {
|
||||||
private var receiveLoop: DispatchWorkItem?
|
private var receiveLoop: Task<Void, Never>?
|
||||||
private var receiveMessages = true
|
private var receiveMessages = true
|
||||||
private var _lastMsgTime = Date.now
|
private var _lastMsgTime = Date.now
|
||||||
|
|
||||||
@@ -424,18 +530,18 @@ class ChatReceiver {
|
|||||||
receiveMessages = true
|
receiveMessages = true
|
||||||
_lastMsgTime = .now
|
_lastMsgTime = .now
|
||||||
if receiveLoop != nil { return }
|
if receiveLoop != nil { return }
|
||||||
let loop = DispatchWorkItem(qos: .default, flags: []) {
|
receiveLoop = Task { await receiveMsgLoop() }
|
||||||
while self.receiveMessages {
|
}
|
||||||
do {
|
|
||||||
processReceivedMsg(try chatRecvMsg())
|
func receiveMsgLoop() async {
|
||||||
self._lastMsgTime = .now
|
let msg = await chatRecvMsg()
|
||||||
} catch {
|
self._lastMsgTime = .now
|
||||||
logger.error("ChatReceiver.start chatRecvMsg error: \(error.localizedDescription)")
|
processReceivedMsg(msg)
|
||||||
}
|
if self.receiveMessages {
|
||||||
}
|
do { try await Task.sleep(nanoseconds: 7_500_000) }
|
||||||
|
catch { logger.error("receiveMsgLoop: Task.sleep error: \(error.localizedDescription)") }
|
||||||
|
await receiveMsgLoop()
|
||||||
}
|
}
|
||||||
receiveLoop = loop
|
|
||||||
DispatchQueue.global().async(execute: loop)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func stop() {
|
func stop() {
|
||||||
@@ -468,20 +574,20 @@ func processReceivedMsg(_ res: ChatResponse) {
|
|||||||
chatModel.updateChatInfo(cInfo)
|
chatModel.updateChatInfo(cInfo)
|
||||||
}
|
}
|
||||||
case let .contactSubscribed(contact):
|
case let .contactSubscribed(contact):
|
||||||
chatModel.updateContact(contact)
|
processContactSubscribed(contact)
|
||||||
chatModel.updateNetworkStatus(contact, .connected)
|
|
||||||
case let .contactDisconnected(contact):
|
case let .contactDisconnected(contact):
|
||||||
chatModel.updateContact(contact)
|
chatModel.updateContact(contact)
|
||||||
chatModel.updateNetworkStatus(contact, .disconnected)
|
chatModel.updateNetworkStatus(contact, .disconnected)
|
||||||
case let .contactSubError(contact, chatError):
|
case let .contactSubError(contact, chatError):
|
||||||
chatModel.updateContact(contact)
|
processContactSubError(contact, chatError)
|
||||||
var err: String
|
case let .contactSubSummary(contactSubscriptions):
|
||||||
switch chatError {
|
for sub in contactSubscriptions {
|
||||||
case .errorAgent(agentError: .BROKER(brokerErr: .NETWORK)): err = "network"
|
if let err = sub.contactError {
|
||||||
case .errorAgent(agentError: .SMP(smpErr: .AUTH)): err = "contact deleted"
|
processContactSubError(sub.contact, err)
|
||||||
default: err = String(describing: chatError)
|
} else {
|
||||||
|
processContactSubscribed(sub.contact)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
chatModel.updateNetworkStatus(contact, .error(err))
|
|
||||||
case let .newChatItem(aChatItem):
|
case let .newChatItem(aChatItem):
|
||||||
let cInfo = aChatItem.chatInfo
|
let cInfo = aChatItem.chatInfo
|
||||||
let cItem = aChatItem.chatItem
|
let cItem = aChatItem.chatItem
|
||||||
@@ -492,6 +598,13 @@ func processReceivedMsg(_ res: ChatResponse) {
|
|||||||
let cItem = aChatItem.chatItem
|
let cItem = aChatItem.chatItem
|
||||||
if chatModel.upsertChatItem(cInfo, cItem) {
|
if chatModel.upsertChatItem(cInfo, cItem) {
|
||||||
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
||||||
|
} else if let endTask = chatModel.messageDelivery[cItem.id] {
|
||||||
|
switch cItem.meta.itemStatus {
|
||||||
|
case .sndSent: endTask()
|
||||||
|
case .sndErrorAuth: endTask()
|
||||||
|
case .sndError: endTask()
|
||||||
|
default: break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
logger.debug("unsupported event: \(res.responseType)")
|
logger.debug("unsupported event: \(res.responseType)")
|
||||||
@@ -499,12 +612,30 @@ func processReceivedMsg(_ res: ChatResponse) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func processContactSubscribed(_ contact: Contact) {
|
||||||
|
let m = ChatModel.shared
|
||||||
|
m.updateContact(contact)
|
||||||
|
m.updateNetworkStatus(contact, .connected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func processContactSubError(_ contact: Contact, _ chatError: ChatError) {
|
||||||
|
let m = ChatModel.shared
|
||||||
|
m.updateContact(contact)
|
||||||
|
var err: String
|
||||||
|
switch chatError {
|
||||||
|
case .errorAgent(agentError: .BROKER(brokerErr: .NETWORK)): err = "network"
|
||||||
|
case .errorAgent(agentError: .SMP(smpErr: .AUTH)): err = "contact deleted"
|
||||||
|
default: err = String(describing: chatError)
|
||||||
|
}
|
||||||
|
m.updateNetworkStatus(contact, .error(err))
|
||||||
|
}
|
||||||
|
|
||||||
private struct UserResponse: Decodable {
|
private struct UserResponse: Decodable {
|
||||||
var user: User?
|
var user: User?
|
||||||
var error: String?
|
var error: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
private func chatResponse(_ cjson: UnsafePointer<CChar>) -> ChatResponse {
|
private func chatResponse(_ cjson: UnsafeMutablePointer<CChar>) -> ChatResponse {
|
||||||
let s = String.init(cString: cjson)
|
let s = String.init(cString: cjson)
|
||||||
let d = s.data(using: .utf8)!
|
let d = s.data(using: .utf8)!
|
||||||
// TODO is there a way to do it without copying the data? e.g:
|
// TODO is there a way to do it without copying the data? e.g:
|
||||||
@@ -528,6 +659,7 @@ private func chatResponse(_ cjson: UnsafePointer<CChar>) -> ChatResponse {
|
|||||||
}
|
}
|
||||||
json = prettyJSON(j)
|
json = prettyJSON(j)
|
||||||
}
|
}
|
||||||
|
free(cjson)
|
||||||
return ChatResponse.response(type: type ?? "invalid", json: json ?? s)
|
return ChatResponse.response(type: type ?? "invalid", json: json ?? s)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,7 +674,10 @@ private func getChatCtrl() -> chat_ctrl {
|
|||||||
if let controller = chatController { return controller }
|
if let controller = chatController { return controller }
|
||||||
let dataDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.path + "/mobile_v1"
|
let dataDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.path + "/mobile_v1"
|
||||||
var cstr = dataDir.cString(using: .utf8)!
|
var cstr = dataDir.cString(using: .utf8)!
|
||||||
|
logger.debug("getChatCtrl: chat_init")
|
||||||
|
ChatModel.shared.terminalItems.append(.cmd(.now, .string("chat_init")))
|
||||||
chatController = chat_init(&cstr)
|
chatController = chat_init(&cstr)
|
||||||
|
ChatModel.shared.terminalItems.append(.resp(.now, .response(type: "chat_controller", json: "chat_controller: no details")))
|
||||||
return chatController!
|
return chatController!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
var greeting = "Hello, playground"
|
|
||||||
|
|
||||||
let jsonEncoder = JSONEncoder()
|
|
||||||
|
|
||||||
//jsonDecoder.decode(Test.self, from: "{\"name\":\"hello\",\"id\":1}".data(using: .utf8)!)
|
|
||||||
|
|
||||||
|
|
||||||
var a = [1, 2, 3]
|
|
||||||
|
|
||||||
a.removeAll(where: { $0 == 1} )
|
|
||||||
|
|
||||||
print(a)
|
|
||||||
|
|
||||||
let input = "This is a test with the привет 🙂 URL https://www.hackingwithswift.com to be detected."
|
|
||||||
let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
|
|
||||||
let matches = detector.matches(in: input, options: [], range: NSRange(location: 0, length: input.count))
|
|
||||||
|
|
||||||
print(matches)
|
|
||||||
|
|
||||||
for match in matches {
|
|
||||||
guard let range = Range(match.range, in: input) else { continue }
|
|
||||||
let url = input[range]
|
|
||||||
print(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
let r = try! NSRegularExpression(pattern: "^\\+?[0-9\\.\\(\\)\\-]{7,20}$")
|
|
||||||
|
|
||||||
print(r.firstMatch(in: "+44(0)7448-736-790", options: [], range: NSRange(location: 0, length: "+44(0)7448-736-790".count)) == nil)
|
|
||||||
|
|
||||||
let action: NtfAction? = NtfAction(rawValue: "NTF_ACT_ACCEPT")
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<playground version='5.0' target-platform='ios' buildActiveScheme='true' importAppTypes='true'>
|
|
||||||
<timeline fileName='timeline.xctimeline'/>
|
|
||||||
</playground>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Timeline
|
|
||||||
version = "3.0">
|
|
||||||
<TimelineItems>
|
|
||||||
<LoggerValueHistoryTimelineItem
|
|
||||||
documentLocation = "file:///Users/evgeny/opensource/simplex-chat/simplex-chat/apps/ios/Shared/MyPlayground.playground#CharacterRangeLen=88&CharacterRangeLoc=91&EndingColumnNumber=0&EndingLineNumber=7&StartingColumnNumber=3&StartingLineNumber=6&Timestamp=666087303.155273"
|
|
||||||
selectedRepresentationIndex = "0"
|
|
||||||
shouldTrackSuperviewWidth = "NO">
|
|
||||||
</LoggerValueHistoryTimelineItem>
|
|
||||||
</TimelineItems>
|
|
||||||
</Timeline>
|
|
||||||
@@ -63,12 +63,16 @@ struct ChatInfoView: View {
|
|||||||
title: Text("Delete contact?"),
|
title: Text("Delete contact?"),
|
||||||
message: Text("Contact and all messages will be deleted"),
|
message: Text("Contact and all messages will be deleted"),
|
||||||
primaryButton: .destructive(Text("Delete")) {
|
primaryButton: .destructive(Text("Delete")) {
|
||||||
do {
|
Task {
|
||||||
try apiDeleteChat(type: .direct, id: contact.apiId)
|
do {
|
||||||
chatModel.removeChat(contact.id)
|
try await apiDeleteChat(type: .direct, id: contact.apiId)
|
||||||
showChatInfo = false
|
DispatchQueue.main.async {
|
||||||
} catch let error {
|
chatModel.removeChat(contact.id)
|
||||||
logger.error("ChatInfoView.deleteContactAlert apiDeleteChat error: \(error.localizedDescription)")
|
showChatInfo = false
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
logger.error("ChatInfoView.deleteContactAlert apiDeleteChat error: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
secondaryButton: .cancel()
|
secondaryButton: .cancel()
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ struct CIMetaView: View {
|
|||||||
default: EmptyView()
|
default: EmptyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(chatItem.timestampText)
|
chatItem.timestampText
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,10 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
private let emailRegex = try! NSRegularExpression(pattern: "^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$", options: .caseInsensitive)
|
|
||||||
|
|
||||||
private let phoneRegex = try! NSRegularExpression(pattern: "^\\+?[0-9\\.\\(\\)\\-]{7,20}$")
|
|
||||||
|
|
||||||
private let sentColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.12)
|
private let sentColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.12)
|
||||||
private let sentColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.17)
|
private let sentColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.17)
|
||||||
private let linkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1)
|
private let uiLinkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1)
|
||||||
|
private let linkColor = Color(uiColor: uiLinkColor)
|
||||||
|
|
||||||
struct TextItemView: View {
|
struct TextItemView: View {
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
@@ -58,73 +55,12 @@ struct TextItemView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func messageText(_ chatItem: ChatItem) -> Text {
|
private func reserveSpaceForMeta(_ meta: Text) -> Text {
|
||||||
let s = chatItem.content.text
|
(Text(" ") + meta)
|
||||||
var res: Text
|
|
||||||
if s == "" {
|
|
||||||
res = Text("")
|
|
||||||
} else {
|
|
||||||
let parts = s.split(separator: " ")
|
|
||||||
res = wordToText(parts[0])
|
|
||||||
var i = 1
|
|
||||||
while i < parts.count {
|
|
||||||
res = res + Text(" ") + wordToText(parts[i])
|
|
||||||
i = i + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if case let .groupRcv(groupMember) = chatItem.chatDir {
|
|
||||||
let member = Text(groupMember.memberProfile.displayName).font(.headline)
|
|
||||||
return member + Text(": ") + res
|
|
||||||
} else {
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func reserveSpaceForMeta(_ meta: String) -> Text {
|
|
||||||
Text(" \(meta)")
|
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.clear)
|
.foregroundColor(.clear)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func wordToText(_ s: String.SubSequence) -> Text {
|
|
||||||
let str = String(s)
|
|
||||||
switch true {
|
|
||||||
case s.starts(with: "http://") || s.starts(with: "https://"):
|
|
||||||
return linkText(str, prefix: "")
|
|
||||||
case match(str, emailRegex):
|
|
||||||
return linkText(str, prefix: "mailto:")
|
|
||||||
case match(str, phoneRegex):
|
|
||||||
return linkText(str, prefix: "tel:")
|
|
||||||
default:
|
|
||||||
if (s.count > 1) {
|
|
||||||
switch true {
|
|
||||||
case s.first == "*" && s.last == "*": return mdText(s).bold()
|
|
||||||
case s.first == "_" && s.last == "_": return mdText(s).italic()
|
|
||||||
case s.first == "+" && s.last == "+": return mdText(s).underline()
|
|
||||||
case s.first == "~" && s.last == "~": return mdText(s).strikethrough()
|
|
||||||
default: return Text(s)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Text(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func match(_ s: String, _ regex: NSRegularExpression) -> Bool {
|
|
||||||
regex.firstMatch(in: s, options: [], range: NSRange(location: 0, length: s.count)) != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func linkText(_ s: String, prefix: String) -> Text {
|
|
||||||
Text(AttributedString(s, attributes: AttributeContainer([
|
|
||||||
.link: NSURL(string: prefix + s) as Any,
|
|
||||||
.foregroundColor: linkColor as Any
|
|
||||||
]))).underline()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func mdText(_ s: String.SubSequence) -> Text {
|
|
||||||
Text(s[s.index(s.startIndex, offsetBy: 1)..<s.index(s.endIndex, offsetBy: -1)])
|
|
||||||
}
|
|
||||||
|
|
||||||
private func msgDeliveryError(_ err: String) {
|
private func msgDeliveryError(_ err: String) {
|
||||||
AlertManager.shared.showAlertMsg(
|
AlertManager.shared.showAlertMsg(
|
||||||
title: "Message delivery error",
|
title: "Message delivery error",
|
||||||
@@ -133,6 +69,57 @@ struct TextItemView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func messageText(_ chatItem: ChatItem, preview: Bool = false) -> Text {
|
||||||
|
let s = chatItem.content.text
|
||||||
|
var res: Text
|
||||||
|
if let ft = chatItem.formattedText, ft.count > 0 {
|
||||||
|
res = formattedText(ft[0], preview)
|
||||||
|
var i = 1
|
||||||
|
while i < ft.count {
|
||||||
|
res = res + formattedText(ft[i], preview)
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res = Text(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
if case let .groupRcv(groupMember) = chatItem.chatDir {
|
||||||
|
let m = Text(groupMember.memberProfile.displayName)
|
||||||
|
return (preview ? m : m.font(.headline)) + Text(": ") + res
|
||||||
|
} else {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formattedText(_ ft: FormattedText, _ preview: Bool) -> Text {
|
||||||
|
let t = ft.text
|
||||||
|
if let f = ft.format {
|
||||||
|
switch (f) {
|
||||||
|
case .bold: return Text(t).bold()
|
||||||
|
case .italic: return Text(t).italic()
|
||||||
|
case .strikeThrough: return Text(t).strikethrough()
|
||||||
|
case .snippet: return Text(t).font(.body.monospaced())
|
||||||
|
case .secret: return Text(t).foregroundColor(.clear).underline(color: .primary)
|
||||||
|
case let .colored(color): return Text(t).foregroundColor(color.uiColor)
|
||||||
|
case .uri: return linkText(t, t, preview, prefix: "")
|
||||||
|
case .email: return linkText(t, t, preview, prefix: "mailto:")
|
||||||
|
case .phone: return linkText(t, t.replacingOccurrences(of: " ", with: ""), preview, prefix: "tel:")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Text(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func linkText(_ s: String, _ link: String,
|
||||||
|
_ preview: Bool, prefix: String) -> Text {
|
||||||
|
preview
|
||||||
|
? Text(s).foregroundColor(linkColor).underline(color: linkColor)
|
||||||
|
: Text(AttributedString(s, attributes: AttributeContainer([
|
||||||
|
.link: NSURL(string: prefix + link) as Any,
|
||||||
|
.foregroundColor: uiLinkColor as Any
|
||||||
|
]))).underline()
|
||||||
|
}
|
||||||
|
|
||||||
struct TextItemView_Previews: PreviewProvider {
|
struct TextItemView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
Group{
|
Group{
|
||||||
|
|||||||
@@ -107,17 +107,21 @@ struct ChatView: View {
|
|||||||
func markAllRead() {
|
func markAllRead() {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||||
if chatModel.chatId == chat.id {
|
if chatModel.chatId == chat.id {
|
||||||
markChatRead(chat)
|
Task { await markChatRead(chat) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendMessage(_ msg: String) {
|
func sendMessage(_ msg: String) {
|
||||||
do {
|
Task {
|
||||||
let chatItem = try apiSendMessage(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, msg: .text(msg))
|
do {
|
||||||
chatModel.addChatItem(chat.chatInfo, chatItem)
|
let chatItem = try await apiSendMessage(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, msg: .text(msg))
|
||||||
} catch {
|
DispatchQueue.main.async {
|
||||||
logger.error("ChatView.sendMessage apiSendMessage error: \(error.localizedDescription)")
|
chatModel.addChatItem(chat.chatInfo, chatItem)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
logger.error("ChatView.sendMessage apiSendMessage error: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,13 +27,17 @@ struct ChatListNavLink: View {
|
|||||||
private func chatView() -> some View {
|
private func chatView() -> some View {
|
||||||
ChatView(chat: chat)
|
ChatView(chat: chat)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
do {
|
Task {
|
||||||
let cInfo = chat.chatInfo
|
do {
|
||||||
let chat = try apiGetChat(type: cInfo.chatType, id: cInfo.apiId)
|
let cInfo = chat.chatInfo
|
||||||
chatModel.updateChatInfo(chat.chatInfo)
|
let chat = try await apiGetChat(type: cInfo.chatType, id: cInfo.apiId)
|
||||||
chatModel.chatItems = chat.chatItems
|
DispatchQueue.main.async {
|
||||||
} catch {
|
chatModel.updateChatInfo(chat.chatInfo)
|
||||||
logger.error("ChatListNavLink.chatView apiGetChatItems error: \(error.localizedDescription)")
|
chatModel.chatItems = chat.chatItems
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
logger.error("ChatListNavLink.chatView apiGetChatItems error: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,7 +90,7 @@ struct ChatListNavLink: View {
|
|||||||
|
|
||||||
private func markReadButton() -> some View {
|
private func markReadButton() -> some View {
|
||||||
Button {
|
Button {
|
||||||
markChatRead(chat)
|
Task { await markChatRead(chat) }
|
||||||
} label: {
|
} label: {
|
||||||
Label("Read", systemImage: "checkmark")
|
Label("Read", systemImage: "checkmark")
|
||||||
}
|
}
|
||||||
@@ -96,7 +100,7 @@ struct ChatListNavLink: View {
|
|||||||
private func contactRequestNavLink(_ contactRequest: UserContactRequest) -> some View {
|
private func contactRequestNavLink(_ contactRequest: UserContactRequest) -> some View {
|
||||||
ContactRequestView(contactRequest: contactRequest)
|
ContactRequestView(contactRequest: contactRequest)
|
||||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||||
Button { acceptContactRequest(contactRequest) }
|
Button { Task { await acceptContactRequest(contactRequest) } }
|
||||||
label: { Label("Accept", systemImage: "checkmark") }
|
label: { Label("Accept", systemImage: "checkmark") }
|
||||||
.tint(Color.accentColor)
|
.tint(Color.accentColor)
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
@@ -108,8 +112,8 @@ struct ChatListNavLink: View {
|
|||||||
.frame(height: 80)
|
.frame(height: 80)
|
||||||
.onTapGesture { showContactRequestDialog = true }
|
.onTapGesture { showContactRequestDialog = true }
|
||||||
.confirmationDialog("Connection request", isPresented: $showContactRequestDialog, titleVisibility: .visible) {
|
.confirmationDialog("Connection request", isPresented: $showContactRequestDialog, titleVisibility: .visible) {
|
||||||
Button("Accept contact") { acceptContactRequest(contactRequest) }
|
Button("Accept contact") { Task { await acceptContactRequest(contactRequest) } }
|
||||||
Button("Reject contact (sender NOT notified)") { rejectContactRequest(contactRequest) }
|
Button("Reject contact (sender NOT notified)") { Task { await rejectContactRequest(contactRequest) } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,11 +122,15 @@ struct ChatListNavLink: View {
|
|||||||
title: Text("Delete contact?"),
|
title: Text("Delete contact?"),
|
||||||
message: Text("Contact and all messages will be deleted"),
|
message: Text("Contact and all messages will be deleted"),
|
||||||
primaryButton: .destructive(Text("Delete")) {
|
primaryButton: .destructive(Text("Delete")) {
|
||||||
do {
|
Task {
|
||||||
try apiDeleteChat(type: .direct, id: contact.apiId)
|
do {
|
||||||
chatModel.removeChat(contact.id)
|
try await apiDeleteChat(type: .direct, id: contact.apiId)
|
||||||
} catch let error {
|
DispatchQueue.main.async {
|
||||||
logger.error("ChatListNavLink.deleteContactAlert apiDeleteChat error: \(error.localizedDescription)")
|
chatModel.removeChat(contact.id)
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
logger.error("ChatListNavLink.deleteContactAlert apiDeleteChat error: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
secondaryButton: .cancel()
|
secondaryButton: .cancel()
|
||||||
@@ -141,7 +149,7 @@ struct ChatListNavLink: View {
|
|||||||
title: Text("Reject contact request"),
|
title: Text("Reject contact request"),
|
||||||
message: Text("The sender will NOT be notified"),
|
message: Text("The sender will NOT be notified"),
|
||||||
primaryButton: .destructive(Text("Reject")) {
|
primaryButton: .destructive(Text("Reject")) {
|
||||||
rejectContactRequest(contactRequest)
|
Task { await rejectContactRequest(contactRequest) }
|
||||||
},
|
},
|
||||||
secondaryButton: .cancel()
|
secondaryButton: .cancel()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -80,19 +80,22 @@ struct ChatListView: View {
|
|||||||
logger.debug("ChatListView.connectViaUrlAlert path: \(path)")
|
logger.debug("ChatListView.connectViaUrlAlert path: \(path)")
|
||||||
if (path == "/contact" || path == "/invitation") {
|
if (path == "/contact" || path == "/invitation") {
|
||||||
path.removeFirst()
|
path.removeFirst()
|
||||||
|
let action = path
|
||||||
let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)")
|
let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)")
|
||||||
return Alert(
|
return Alert(
|
||||||
title: Text("Connect via \(path) link?"),
|
title: Text("Connect via \(action) link?"),
|
||||||
message: Text("Your profile will be sent to the contact that you received this link from: \(link)"),
|
message: Text("Your profile will be sent to the contact that you received this link from"),
|
||||||
primaryButton: .default(Text("Connect")) {
|
primaryButton: .default(Text("Connect")) {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
do {
|
Task {
|
||||||
try apiConnect(connReq: link)
|
do {
|
||||||
connectionReqSentAlert(path == "contact" ? .contact : .invitation)
|
try await apiConnect(connReq: link)
|
||||||
} catch {
|
connectionReqSentAlert(action == "contact" ? .contact : .invitation)
|
||||||
let err = error.localizedDescription
|
} catch {
|
||||||
AlertManager.shared.showAlertMsg(title: "Connection error", message: err)
|
let err = error.localizedDescription
|
||||||
logger.debug("ChatListView.connectViaUrlAlert: apiConnect error: \(err)")
|
AlertManager.shared.showAlertMsg(title: "Connection error", message: err)
|
||||||
|
logger.debug("ChatListView.connectViaUrlAlert: apiConnect error: \(err)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ struct ChatPreviewView: View {
|
|||||||
.foregroundColor(chat.chatInfo.ready ? .primary : .secondary)
|
.foregroundColor(chat.chatInfo.ready ? .primary : .secondary)
|
||||||
.frame(maxHeight: .infinity, alignment: .topLeading)
|
.frame(maxHeight: .infinity, alignment: .topLeading)
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(cItem?.timestampText ?? timestampText(chat.chatInfo.createdAt))
|
(cItem?.timestampText ?? timestampText(chat.chatInfo.createdAt))
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.frame(minWidth: 60, alignment: .trailing)
|
.frame(minWidth: 60, alignment: .trailing)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
@@ -51,7 +51,7 @@ struct ChatPreviewView: View {
|
|||||||
|
|
||||||
if let cItem = cItem {
|
if let cItem = cItem {
|
||||||
ZStack(alignment: .topTrailing) {
|
ZStack(alignment: .topTrailing) {
|
||||||
(itemStatusMark(cItem) + Text(chatItemText(cItem)))
|
(itemStatusMark(cItem) + messageText(cItem, preview: true))
|
||||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .topLeading)
|
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .topLeading)
|
||||||
.padding(.leading, 8)
|
.padding(.leading, 8)
|
||||||
.padding(.trailing, 36)
|
.padding(.trailing, 36)
|
||||||
@@ -91,14 +91,6 @@ struct ChatPreviewView: View {
|
|||||||
default: return Text("")
|
default: return Text("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func chatItemText(_ cItem: ChatItem) -> String {
|
|
||||||
let t = cItem.content.text
|
|
||||||
if case let .groupRcv(groupMember) = cItem.chatDir {
|
|
||||||
return groupMember.memberProfile.displayName + ": " + t
|
|
||||||
}
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ChatPreviewView_Previews: PreviewProvider {
|
struct ChatPreviewView_Previews: PreviewProvider {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ struct ContactRequestView: View {
|
|||||||
.padding(.top, 4)
|
.padding(.top, 4)
|
||||||
.frame(maxHeight: .infinity, alignment: .topLeading)
|
.frame(maxHeight: .infinity, alignment: .topLeading)
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(timestampText(contactRequest.createdAt))
|
timestampText(contactRequest.createdAt)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.padding(.trailing, 8)
|
.padding(.trailing, 8)
|
||||||
.padding(.top, 4)
|
.padding(.top, 4)
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ struct AddContactView: View {
|
|||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
QRCode(uri: connReqInvitation)
|
QRCode(uri: connReqInvitation)
|
||||||
.padding()
|
.padding()
|
||||||
Text("If you can't show QR code, you can share the invitation link via any channel")
|
(Text("If you cannot meet in person, you can ") +
|
||||||
|
Text("scan QR code in the video call").bold() +
|
||||||
|
Text(", or you can share the invitation link via any other channel."))
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
|||||||
@@ -33,12 +33,14 @@ struct ConnectContactView: View {
|
|||||||
func processQRCode(_ resp: Result<ScanResult, ScanError>) {
|
func processQRCode(_ resp: Result<ScanResult, ScanError>) {
|
||||||
switch resp {
|
switch resp {
|
||||||
case let .success(r):
|
case let .success(r):
|
||||||
do {
|
Task {
|
||||||
try apiConnect(connReq: r.string)
|
do {
|
||||||
completed(nil)
|
try await apiConnect(connReq: r.string)
|
||||||
} catch {
|
completed(nil)
|
||||||
logger.error("ConnectContactView.processQRCode apiConnect error: \(error.localizedDescription)")
|
} catch {
|
||||||
completed(error)
|
logger.error("ConnectContactView.processQRCode apiConnect error: \(error.localizedDescription)")
|
||||||
|
completed(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case let .failure(e):
|
case let .failure(e):
|
||||||
logger.error("ConnectContactView.processQRCode QR code error: \(e.localizedDescription)")
|
logger.error("ConnectContactView.processQRCode QR code error: \(e.localizedDescription)")
|
||||||
|
|||||||