diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 0000000000..e30a67f475 --- /dev/null +++ b/PRIVACY.md @@ -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 diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index b660ad0476..34b4cf6ab2 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -9,10 +9,10 @@ android { defaultConfig { applicationId "chat.simplex.app" - minSdk 26 + minSdk 29 targetSdk 32 - versionCode 3 - versionName "0.2" + versionCode 8 + versionName "0.4.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" ndk { @@ -40,6 +40,12 @@ android { } kotlinOptions { 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 { cmake { @@ -66,6 +72,7 @@ dependencies { implementation "androidx.compose.material:material:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" 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 'org.jetbrains.kotlinx:kotlinx-datetime:0.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 "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" implementation "androidx.camera:camera-core:${camerax_version}" implementation "androidx.camera:camera-camera2:${camerax_version}" diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index a935883631..20fff5ec83 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -2,180 +2,119 @@ package chat.simplex.app import android.app.Application import android.content.Intent +import android.net.Uri import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.viewModels +import androidx.compose.foundation.background 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.ui.text.ExperimentalTextApi +import androidx.compose.ui.Modifier import androidx.lifecycle.AndroidViewModel -import androidx.navigation.* -import androidx.navigation.compose.* -import chat.simplex.app.model.* +import chat.simplex.app.model.ChatModel +import chat.simplex.app.model.NtfManager import chat.simplex.app.ui.theme.SimpleXTheme -import chat.simplex.app.views.* -import chat.simplex.app.views.chat.ChatInfoView +import chat.simplex.app.views.SplashView +import chat.simplex.app.views.WelcomeView import chat.simplex.app.views.chat.ChatView 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.newchat.* -import chat.simplex.app.views.usersettings.* -import com.google.accompanist.insets.ExperimentalAnimatedInsets -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.serialization.decodeFromString -@ExperimentalTextApi -@DelicateCoroutinesApi -@ExperimentalAnimatedInsets -@ExperimentalPermissionsApi -@ExperimentalMaterialApi +//import kotlinx.serialization.decodeFromString + class MainActivity: ComponentActivity() { private val vm by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // testJson() - connectIfOpenedViaUri(intent, vm.chatModel) + processIntent(intent, vm.chatModel) +// vm.app.initiateBackgroundWork() setContent { SimpleXTheme { - Navigation(vm.chatModel) - } - } - } -} - -@DelicateCoroutinesApi -class SimplexViewModel(application: Application): AndroidViewModel(application) { - val chatModel = getApplication().chatModel -} - -@ExperimentalTextApi -@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) - } -} - -@ExperimentalTextApi -@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 } - } + Surface( + Modifier + .background(MaterialTheme.colors.background) + .fillMaxSize() + ) { + MainPage(vm.chatModel) } } - 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.UserProfile.route) { - UserProfileView(chatModel, nav) - } - composable(route = Pages.UserAddress.route) { - UserAddressView(chatModel, nav) - } - composable(route = Pages.Help.route) { - HelpView(chatModel, nav) - } - composable(route = Pages.Markdown.route) { - MarkdownHelpView(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 UserProfile: Pages("user_profile") - object UserAddress: Pages("user_address") - object Help: Pages("help") - object Markdown: Pages("markdown") -} - -@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) - } - } - ) - } } } } -fun testJson() { - val str = """ - {} - """.trimIndent() - - println(json.decodeFromString(str)) +class SimplexViewModel(application: Application): AndroidViewModel(application) { + val app = getApplication() + 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(str)) +//} diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt index cb8fa644fe..28e5fb91a4 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt @@ -1,21 +1,20 @@ package chat.simplex.app import android.app.Application -import android.net.LocalServerSocket +import android.net.* import android.util.Log -import androidx.compose.material.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import chat.simplex.app.model.ChatController -import chat.simplex.app.model.ChatModel +import androidx.lifecycle.* +import androidx.work.* +import chat.simplex.app.model.* import chat.simplex.app.views.helpers.withApi -import kotlinx.coroutines.DelicateCoroutinesApi import java.io.BufferedReader import java.io.InputStreamReader import java.util.* import java.util.concurrent.Semaphore import kotlin.concurrent.thread +const val TAG = "SIMPLEX" + // ghc's rts external fun initHS() // android-support @@ -27,15 +26,18 @@ external fun chatInit(path: String): ChatCtrl external fun chatSendCmd(ctrl: ChatCtrl, msg: String) : String external fun chatRecvMsg(ctrl: ChatCtrl) : String -@DelicateCoroutinesApi +//class SimplexApp: Application(), LifecycleEventObserver { class SimplexApp: Application() { private lateinit var controller: ChatController lateinit var chatModel: ChatModel + private lateinit var ntfManager: NtfManager override fun onCreate() { super.onCreate() +// ProcessLifecycleOwner.get().lifecycle.addObserver(this) + ntfManager = NtfManager(applicationContext) val ctrl = chatInit(applicationContext.filesDir.toString()) - controller = ChatController(ctrl, AlertManager()) + controller = ChatController(ctrl, ntfManager, applicationContext) chatModel = controller.chatModel withApi { val user = controller.apiGetActiveUser() @@ -43,71 +45,13 @@ class SimplexApp: Application() { } } - class AlertManager { - var alertView = mutableStateOf<(@Composable () -> Unit)?>(null) - var presentAlert = mutableStateOf(false) - - fun showAlert(alert: @Composable () -> Unit) { - Log.d("SIMPLEX", "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) } - } - ) - } - } - } +// override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { +// Log.d(TAG, "onStateChanged: $event") +// if (event == Lifecycle.Event.ON_STOP) { +// Log.e(TAG, "BGManager schedule ${Clock.System.now()}") +// BGManager.schedule(applicationContext) +// } +// } companion object { init { @@ -115,12 +59,12 @@ class SimplexApp: Application() { val s = Semaphore(0) thread(name="stdout/stderr pipe") { - Log.d("SIMPLEX", "starting server") + Log.d(TAG, "starting server") val server = LocalServerSocket(socketName) - Log.d("SIMPLEX", "started server") + Log.d(TAG, "started server") s.release() val receiver = server.accept() - Log.d("SIMPLEX", "started receiver") + Log.d(TAG, "started receiver") val logbuffer = FifoQueue(500) if (receiver != null) { val inStream = receiver.inputStream @@ -129,7 +73,7 @@ class SimplexApp: Application() { while(true) { val line = input.readLine() ?: break - Log.d("SIMPLEX (stdout/stderr)", line) + Log.w("$TAG (stdout/stderr)", line) logbuffer.add(line) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/BGManager.kt b/apps/android/app/src/main/java/chat/simplex/app/model/BGManager.kt new file mode 100644 index 0000000000..d274a98f6d --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/model/BGManager.kt @@ -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() + .setInitialDelay(Duration.ofMinutes(10)) + .setConstraints(constraints) + .build() + WorkManager.getInstance(appContext) + .enqueue(request) + } + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 82b1dbf827..92c96064c8 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -7,19 +7,20 @@ 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.SimplexApp -import chat.simplex.app.ui.theme.* -import kotlinx.coroutines.DelicateCoroutinesApi +import chat.simplex.app.ui.theme.SecretColor +import chat.simplex.app.ui.theme.SimplexBlue import kotlinx.datetime.* -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable +import kotlinx.serialization.* +import kotlinx.serialization.builtins.IntArraySerializer +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* +import kotlinx.serialization.json.* +import kotlinx.serialization.modules.SerializersModule -@DelicateCoroutinesApi -class ChatModel(val controller: ChatController, val alertManager: SimplexApp.AlertManager) { +class ChatModel(val controller: ChatController) { var currentUser = mutableStateOf(null) var userCreated = mutableStateOf(null) var chats = mutableStateListOf() - var chatsLoaded = mutableStateOf(null) var chatId = mutableStateOf(null) var chatItems = mutableStateListOf() @@ -590,14 +591,57 @@ sealed class CIContent { } } -@Serializable +@Serializable(with = MsgContentSerializer::class) sealed class MsgContent { abstract val text: String - abstract val cmdString: String - @Serializable @SerialName("text") - class MCText(override val text: String): MsgContent() { - override val cmdString get() = "text $text" + class MCText(override val text: String): MsgContent() + class MCUnknown(val type: String? = null, override val text: String, val json: JsonElement): MsgContent() + + val cmdString: String get() = when (this) { + is MCText -> "text $text" + is MCUnknown -> "json $json" + } +} + +object MsgContentSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildSerialDescriptor("MsgContent", PolymorphicKind.SEALED) { + element("MCText", buildClassSerialDescriptor("MCText") { + element("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) } } @@ -654,7 +698,7 @@ enum class FormatColor(val color: String) { val uiColor: Color @Composable get() = when (this) { red -> Color.Red green -> Color.Green - blue -> Color.Blue + blue -> SimplexBlue yellow -> Color.Yellow cyan -> Color.Cyan magenta -> Color.Magenta diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt b/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt new file mode 100644 index 0000000000..f5b54fd8eb --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt @@ -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() + 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) + } + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index c8f09cfe90..aac062974b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -1,10 +1,15 @@ package chat.simplex.app.model +import android.app.ActivityManager +import android.app.ActivityManager.RunningAppProcessInfo +import android.content.Context import android.util.Log import androidx.compose.runtime.mutableStateOf import chat.simplex.app.* +import chat.simplex.app.views.helpers.AlertManager import chat.simplex.app.views.helpers.withApi -import kotlinx.coroutines.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.serialization.* @@ -14,23 +19,21 @@ import kotlin.concurrent.thread typealias ChatCtrl = Long -@DelicateCoroutinesApi -open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.AlertManager) { - var chatModel = ChatModel(this, alertManager) +open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val appContext: Context) { + var chatModel = ChatModel(this) suspend fun startChat(u: User) { - chatModel.currentUser = mutableStateOf(u) - chatModel.userCreated.value = true - Log.d("SIMPLEX (user)", u.toString()) + Log.d(TAG, "user: $u") try { apiStartChat() chatModel.userAddress.value = apiGetUserAddress() chatModel.chats.addAll(apiGetChats()) - chatModel.chatsLoaded.value = true + chatModel.currentUser = mutableStateOf(u) + chatModel.userCreated.value = true startReceiver() - Log.d("SIMPLEX", "started chat") + Log.d(TAG, "started chat") } catch(e: Error) { - Log.d("SIMPLEX", "failed starting chat $e") + Log.e(TAG, "failed starting chat $e") throw e } } @@ -41,16 +44,28 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert } } + open fun isAppOnForeground(context: Context): Boolean { + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + 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 { return withContext(Dispatchers.IO) { val c = cmd.cmdString chatModel.terminalItems.add(TerminalItem.cmd(cmd)) val json = chatSendCmd(ctrl, c) - Log.d("SIMPLEX", "sendCmd: ${cmd.cmdType}") + Log.d(TAG, "sendCmd: ${cmd.cmdType}") 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) { - Log.d("SIMPLEX", "sendCmd response json $json") + Log.d(TAG, "sendCmd response json $json") } chatModel.terminalItems.add(TerminalItem.resp(r.resp)) r.resp @@ -61,8 +76,8 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert return withContext(Dispatchers.IO) { val json = chatRecvMsg(ctrl) val r = APIResponse.decodeStr(json).resp - Log.d("SIMPLEX", "chatRecvMsg: ${r.responseType}") - if (r is CR.Response || r is CR.Invalid) Log.d("SIMPLEX", "chatRecvMsg json: $json") + Log.d(TAG, "chatRecvMsg: ${r.responseType}") + if (r is CR.Response || r is CR.Invalid) Log.d(TAG, "chatRecvMsg json: $json") r } } @@ -75,7 +90,7 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert suspend fun apiGetActiveUser(): User? { val r = sendCmd(CC.ShowActiveUser()) 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 return null } @@ -83,7 +98,7 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert suspend fun apiCreateActiveUser(p: Profile): User { val r = sendCmd(CC.CreateActiveUser(p)) 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}") } @@ -102,21 +117,21 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert suspend fun apiGetChat(type: ChatType, id: Long): Chat? { val r = sendCmd(CC.ApiGetChat(type, id)) 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 } suspend fun apiSendMessage(type: ChatType, id: Long, mc: MsgContent): AChatItem? { val r = sendCmd(CC.ApiSendMessage(type, id, mc)) 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 } suspend fun apiAddContact(): String? { val r = sendCmd(CC.AddContact()) 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 } @@ -125,14 +140,14 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert when { r is CR.SentConfirmation || r is CR.SentInvitation -> return true 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" ) return false } r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat && 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." ) return false @@ -146,20 +161,19 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert suspend fun apiDeleteChat(type: ChatType, id: Long): Boolean { val r = sendCmd(CC.ApiDeleteChat(type, id)) - when { - r is CR.ContactDeleted -> return true // TODO groups - r is CR.ChatCmdError -> { + when (r) { + is CR.ContactDeleted -> return true // TODO groups + is CR.ChatCmdError -> { val e = r.chatError if (e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.ContactGroups) { - alertManager.showAlertMsg( + AlertManager.shared.showAlertMsg( "Can't delete contact!", "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 } @@ -167,21 +181,21 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert val r = sendCmd(CC.UpdateProfile(profile)) if (r is CR.UserProfileNoChange) return profile 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 } suspend fun apiCreateUserAddress(): String? { val r = sendCmd(CC.CreateMyAddress()) 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 } suspend fun apiDeleteUserAddress(): Boolean { val r = sendCmd(CC.DeleteMyAddress()) 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 } @@ -192,35 +206,35 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert && r.chatError.storeError is StoreError.UserContactLinkNotFound) { return null } - Log.d("SIMPLEX", "apiGetUserAddress bad response: ${r.responseType} ${r.details}") + Log.e(TAG, "apiGetUserAddress bad response: ${r.responseType} ${r.details}") return null } suspend fun apiAcceptContactRequest(contactReqId: Long): Contact? { val r = sendCmd(CC.ApiAcceptContact(contactReqId)) 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 } suspend fun apiRejectContactRequest(contactReqId: Long): Boolean { val r = sendCmd(CC.ApiRejectContact(contactReqId)) 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 } suspend fun apiChatRead(type: ChatType, id: Long, range: CC.ItemRange): Boolean { val r = sendCmd(CC.ApiChatRead(type, id, range)) 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 } fun apiErrorAlert(method: String, title: String, r: CR) { val errMsg = "${r.responseType}: ${r.details}" - Log.e("SIMPLEX", "$method bad response: $errMsg") - alertManager.showAlertMsg(title, errMsg) + Log.e(TAG, "$method bad response: $errMsg") + AlertManager.shared.showAlertMsg(title, errMsg) } fun processReceivedMsg(r: CR) { @@ -260,7 +274,9 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert val cInfo = r.chatItem.chatInfo val cItem = r.chatItem.chatItem chatModel.addChatItem(cInfo, cItem) -// NtfManager.shared.notifyMessageReceived(cInfo, cItem) + if (!isAppOnForeground(appContext) || chatModel.chatId.value != cInfo.id) { + ntfManager.notifyMessageReceived(cInfo, cItem) + } } // case let .chatItemUpdated(aChatItem): // let cInfo = aChatItem.chatInfo @@ -268,9 +284,8 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert // if chatModel.upsertChatItem(cInfo, cItem) { // NtfManager.shared.notifyMessageReceived(cInfo, cItem) // } -// default: -// logger.debug("unsupported event: \(res.responseType)") -// } + else -> + Log.d(TAG , "unsupported event: ${r.responseType}") } } @@ -394,6 +409,7 @@ class APIResponse(val resp: CR, val corr: String? = null) { sealed class CR { @Serializable @SerialName("activeUser") class ActiveUser(val user: User): CR() @Serializable @SerialName("chatStarted") class ChatStarted: CR() + @Serializable @SerialName("chatRunning") class ChatRunning: CR() @Serializable @SerialName("apiChats") class ApiChats(val chats: List): CR() @Serializable @SerialName("apiChat") class ApiChat(val chat: Chat): CR() @Serializable @SerialName("invitation") class Invitation(val connReqInvitation: String): CR() @@ -430,8 +446,9 @@ sealed class CR { val responseType: String get() = when(this) { is ActiveUser -> "activeUser" is ChatStarted -> "chatStarted" + is ChatRunning -> "chatRunning" is ApiChats -> "apiChats" - is ApiChat -> "apiChats" + is ApiChat -> "apiChat" is Invitation -> "invitation" is SentConfirmation -> "sentConfirmation" is SentInvitation -> "sentInvitation" @@ -467,6 +484,7 @@ sealed class CR { val details: String get() = when(this) { is ActiveUser -> json.encodeToString(user) is ChatStarted -> noDetails() + is ChatRunning -> noDetails() is ApiChats -> json.encodeToString(chats) is ApiChat -> json.encodeToString(chat) is Invitation -> connReqInvitation diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/SplashView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/SplashView.kt index 503a50e211..f37a6fca8e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/SplashView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/SplashView.kt @@ -1,22 +1,18 @@ 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.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import chat.simplex.app.R @Composable fun SplashView() { - Box(modifier = Modifier - .fillMaxSize() - .background(color = Color.White) + Surface( + Modifier + .background(MaterialTheme.colors.background) + .fillMaxSize() ) { // Image( // painter = painterResource(R.drawable.logo), diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt index 4d83b38b1d..5a9e3c0411 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt @@ -1,8 +1,10 @@ package chat.simplex.app.views import android.content.res.Configuration +import androidx.activity.compose.BackHandler 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.text.selection.SelectionContainer import androidx.compose.material.* @@ -15,21 +17,20 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.navigation.NavController import chat.simplex.app.model.* import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.chat.SendMsgView import chat.simplex.app.views.helpers.CloseSheetBar 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.navigationBarsWithImePadding -import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.launch -@DelicateCoroutinesApi @Composable -fun TerminalView(chatModel: ChatModel, nav: NavController) { - TerminalLayout(chatModel.terminalItems, nav::popBackStack, nav::navigate) { cmd -> +fun TerminalView(chatModel: ChatModel, close: () -> Unit) { + BackHandler(onBack = close) + TerminalLayout(chatModel.terminalItems, close) { cmd -> withApi { // show "in progress" chatModel.controller.sendCmd(CC.Console(cmd)) @@ -39,39 +40,45 @@ fun TerminalView(chatModel: ChatModel, nav: NavController) { } @Composable -fun TerminalLayout(terminalItems: List , close: () -> Unit, navigate: (String) -> Unit, - sendCommand: (String) -> Unit) { +fun TerminalLayout(terminalItems: List , close: () -> Unit, sendCommand: (String) -> Unit) { ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { Scaffold( topBar = { CloseSheetBar(close) }, bottomBar = { SendMsgView(sendCommand) }, modifier = Modifier.navigationBarsWithImePadding() ) { contentPadding -> - Box( + Surface( modifier = Modifier .padding(contentPadding) .fillMaxWidth() .background(MaterialTheme.colors.background) ) { - TerminalLog(terminalItems, navigate) + TerminalLog(terminalItems) } } } } @Composable -fun TerminalLog(terminalItems: List, navigate: (String) -> Unit) { +fun TerminalLog(terminalItems: List) { val listState = rememberLazyListState() val scope = rememberCoroutineScope() LazyColumn(state = listState) { items(terminalItems) { item -> - Text("${item.date.toString().subSequence(11, 19)} ${item.label}", - style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 18.sp, color = MaterialTheme.colors.primary), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .padding(horizontal = 8.dp, vertical = 4.dp) - .clickable { navigate("details/${item.id}") }) + Text("${item.date.toString().subSequence(11, 19)} ${item.label}", + style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 18.sp, color = MaterialTheme.colors.primary), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + 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() if (len > 1) { @@ -82,22 +89,6 @@ fun TerminalLog(terminalItems: List, navigate: (String) -> Unit) { } } -@Composable -fun DetailView(identifier: Long, terminalItems: List, nav: NavController){ - Surface( - Modifier - .background(MaterialTheme.colors.background) - .fillMaxSize() - ) { - Column { - CloseSheetBar(nav::popBackStack) - SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) { - Text((terminalItems.firstOrNull { it.id == identifier })?.details ?: "") - } - } - } -} - @Preview(showBackground = true) @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES, @@ -110,7 +101,6 @@ fun PreviewTerminalLayout() { TerminalLayout( terminalItems = TerminalItem.sampleData, close = {}, - navigate = {}, sendCommand = {} ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt index fcfafb82d0..721553f2c0 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt @@ -19,11 +19,9 @@ import chat.simplex.app.model.Profile import chat.simplex.app.views.helpers.withApi import com.google.accompanist.insets.ProvideWindowInsets import com.google.accompanist.insets.navigationBarsWithImePadding -import kotlinx.coroutines.DelicateCoroutinesApi -@DelicateCoroutinesApi @Composable -fun WelcomeView(chatModel: ChatModel, routeHome: () -> Unit) { +fun WelcomeView(chatModel: ChatModel) { ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { Box( modifier = Modifier @@ -60,7 +58,7 @@ fun WelcomeView(chatModel: ChatModel, routeHome: () -> Unit) { color = MaterialTheme.colors.onBackground ) Spacer(Modifier.height(24.dp)) - CreateProfilePanel(chatModel, routeHome) + CreateProfilePanel(chatModel) } } } @@ -71,9 +69,8 @@ fun isValidDisplayName(name: String) : Boolean { return (name.firstOrNull { it.isWhitespace() }) == null } -@DelicateCoroutinesApi @Composable -fun CreateProfilePanel(chatModel: ChatModel, routeHome: () -> Unit) { +fun CreateProfilePanel(chatModel: ChatModel) { var displayName by remember { mutableStateOf("") } var fullName by remember { mutableStateOf("") } @@ -154,10 +151,9 @@ fun CreateProfilePanel(chatModel: ChatModel, routeHome: () -> Unit) { Profile(displayName, fullName) ) chatModel.controller.startChat(user) - routeHome() } }, - enabled = displayName.isNotEmpty() - ) { Text("Create")} + enabled = (displayName.isNotEmpty() && isValidDisplayName(displayName)) + ) { Text("Create") } } -} \ No newline at end of file +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt index 4f1dc1b823..cc272b8fb8 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt @@ -1,5 +1,6 @@ package chat.simplex.app.views.chat +import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.layout.* 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.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import chat.simplex.app.Pages import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.* -import kotlinx.coroutines.DelicateCoroutinesApi -@DelicateCoroutinesApi @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 } if (chat != null) { ChatInfoLayout(chat, - close = { nav.popBackStack() }, + close = close, deleteContact = { - chatModel.alertManager.showAlertMsg( + AlertManager.shared.showAlertMsg( title = "Delete contact?", text = "Contact and all messages will be deleted - this cannot be undone!", confirmText = "Delete", @@ -39,7 +37,8 @@ fun ChatInfoView(chatModel: ChatModel, nav: NavController) { val r = chatModel.controller.apiDeleteChat(cInfo.chatType, cInfo.apiId) if (r) { 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)) - Box(Modifier.padding(24.dp)) { + Box(Modifier.padding(48.dp)) { SimpleButton( "Delete contact", icon = Icons.Outlined.Delete, color = Color.Red, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index 5b6e3044f5..d509cd97b3 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -1,6 +1,8 @@ package chat.simplex.app.views.chat import android.content.res.Configuration +import android.util.Log +import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -12,68 +14,65 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.text.ExperimentalTextApi import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import chat.simplex.app.Pages +import chat.simplex.app.TAG import chat.simplex.app.model.* import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.chat.item.ChatItemView import chat.simplex.app.views.helpers.ChatInfoImage import chat.simplex.app.views.helpers.withApi -import com.google.accompanist.insets.* -import kotlinx.coroutines.* +import chat.simplex.app.views.newchat.ModalManager +import com.google.accompanist.insets.ProvideWindowInsets +import com.google.accompanist.insets.navigationBarsWithImePadding +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlinx.datetime.Clock -@ExperimentalTextApi -@ExperimentalAnimatedInsets -@DelicateCoroutinesApi @Composable -fun ChatView(chatModel: ChatModel, nav: NavController) { - if (chatModel.chatId.value != null && chatModel.chats.count() > 0) { - val chat: Chat? = chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value } - if (chat != null) { - // TODO a more advanced version would mark as read only if in view - LaunchedEffect(chat.chatItems) { - delay(1000L) - if (chat.chatItems.count() > 0) { - chatModel.markChatItemsRead(chat.chatInfo) - withApi { - chatModel.controller.apiChatRead( - chat.chatInfo.chatType, - chat.chatInfo.apiId, - CC.ItemRange(chat.chatStats.minUnreadItemId, chat.chatItems.last().id) - ) - } +fun ChatView(chatModel: ChatModel) { + val chat: Chat? = chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value } + if (chat == null) { + chatModel.chatId.value = null + } else { + BackHandler { chatModel.chatId.value = null } + // TODO a more advanced version would mark as read only if in view + LaunchedEffect(chat.chatItems) { + Log.d(TAG, "ChatView ${chatModel.chatId.value}: LaunchedEffect") + delay(1000L) + if (chat.chatItems.count() > 0) { + chatModel.markChatItemsRead(chat.chatInfo) + withApi { + chatModel.controller.apiChatRead( + 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) + } + } + ) } } -@ExperimentalTextApi -@DelicateCoroutinesApi -@ExperimentalAnimatedInsets @Composable fun ChatLayout( chat: Chat, chatItems: List, @@ -81,19 +80,16 @@ fun ChatLayout( info: () -> Unit, sendMessage: (String) -> Unit ) { - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { - Scaffold( - topBar = { ChatInfoToolbar(chat, back, info) }, - bottomBar = { SendMsgView(sendMessage) }, - modifier = Modifier.navigationBarsWithImePadding() - ) { contentPadding -> - Box( - modifier = Modifier - .padding(contentPadding) - .fillMaxWidth() - .background(MaterialTheme.colors.background) - ) { - ChatItemsList(chatItems) + Surface(Modifier.fillMaxWidth().background(MaterialTheme.colors.background)) { + ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { + Scaffold( + topBar = { ChatInfoToolbar(chat, back, info) }, + bottomBar = { SendMsgView(sendMessage) }, + modifier = Modifier.navigationBarsWithImePadding() + ) { contentPadding -> + Box(Modifier.padding(contentPadding)) { + ChatItemsList(chatItems) + } } } } @@ -101,7 +97,10 @@ fun ChatLayout( @Composable 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 ) { IconButton(onClick = back) { @@ -136,9 +135,6 @@ fun ChatInfoToolbar(chat: Chat, back: () -> Unit, info: () -> Unit) { } } -@ExperimentalTextApi -@DelicateCoroutinesApi -@ExperimentalAnimatedInsets @Composable fun ChatItemsList(chatItems: List) { val listState = rememberLazyListState() @@ -157,8 +153,6 @@ fun ChatItemsList(chatItems: List) { } } -@ExperimentalTextApi -@ExperimentalAnimatedInsets @Preview(showBackground = true) @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt index d9df249303..639933dde5 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt @@ -15,9 +15,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.ui.theme.SimpleXTheme @Composable @@ -33,6 +35,7 @@ fun SendMsgView(sendMessage: (String) -> Unit) { autoCorrect = true ), modifier = Modifier.padding(8.dp), + cursorBrush = SolidColor(HighOrLowlight), decorationBox = { innerTextField -> Surface( shape = RoundedCornerShape(18.dp), diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt index fcbd726a6d..aafcb7d041 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt @@ -1,6 +1,5 @@ package chat.simplex.app.views.chat.item -import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt index 8a252f714b..ddcbbdc1a5 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.UriHandler -import androidx.compose.ui.text.ExperimentalTextApi import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import chat.simplex.app.model.CIDirection @@ -13,7 +12,6 @@ import chat.simplex.app.model.ChatItem import chat.simplex.app.ui.theme.SimpleXTheme import kotlinx.datetime.Clock -@ExperimentalTextApi @Composable fun ChatItemView(chatItem: ChatItem, uriHandler: UriHandler? = null) { val sent = chatItem.chatDir.sent @@ -33,7 +31,6 @@ fun ChatItemView(chatItem: ChatItem, uriHandler: UriHandler? = null) { } } -@ExperimentalTextApi @Preview @Composable fun PreviewChatItemView() { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt index 5be27d0e57..df2528dd25 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt @@ -1,6 +1,7 @@ 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.text.ClickableText import androidx.compose.foundation.text.selection.SelectionContainer @@ -15,8 +16,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.LightGray +import chat.simplex.app.model.CIDirection +import chat.simplex.app.model.ChatItem import chat.simplex.app.ui.theme.SimpleXTheme import kotlinx.datetime.Clock @@ -24,7 +25,6 @@ import kotlinx.datetime.Clock val SentColorLight = Color(0x1E45B8FF) val ReceivedColorLight = Color(0x1EB1B0B5) -@ExperimentalTextApi @Composable fun TextItemView(chatItem: ChatItem, uriHandler: UriHandler? = null) { val sent = chatItem.chatDir.sent @@ -55,11 +55,10 @@ fun appendGroupMember(b: AnnotatedString.Builder, chatItem: ChatItem, groupMembe } } -@ExperimentalTextApi @Composable fun MarkdownText ( chatItem: ChatItem, - style: TextStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground), + style: TextStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onSurface), maxLines: Int = Int.MAX_VALUE, overflow: TextOverflow = TextOverflow.Clip, uriHandler: UriHandler? = null, @@ -110,7 +109,6 @@ fun MarkdownText ( } } -@ExperimentalTextApi @Preview @Composable fun PreviewTextItemViewSnd() { @@ -123,7 +121,6 @@ fun PreviewTextItemViewSnd() { } } -@ExperimentalTextApi @Preview @Composable fun PreviewTextItemViewRcv() { @@ -136,7 +133,6 @@ fun PreviewTextItemViewRcv() { } } -@ExperimentalTextApi @Preview @Composable fun PreviewTextItemViewLong() { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatHelpView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatHelpView.kt index 8fb912c6d5..c12239f580 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatHelpView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatHelpView.kt @@ -1,7 +1,6 @@ package chat.simplex.app.views.chat import android.content.res.Configuration -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -18,35 +17,28 @@ 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, doAddContact: Boolean) { +fun ChatHelpView(addContact: (() -> Unit)? = null) { Column( - Modifier - .fillMaxWidth() - .background(MaterialTheme.colors.background), horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(10.dp) ) { val uriHandler = LocalUriHandler.current - Text( - "Thank you for installing SimpleX Chat!", - color = MaterialTheme.colors.onBackground - ) + Text("Thank you for installing SimpleX Chat!") Text( buildAnnotatedString { - withStyle(SpanStyle(color = MaterialTheme.colors.onBackground)) { - append("You can ") - } + append("You can ") withStyle(SpanStyle(color = MaterialTheme.colors.primary)) { append("connect to SimpleX team") } - withStyle(SpanStyle(color = MaterialTheme.colors.onBackground)) { - append(".") - } + append(".") }, - modifier = Modifier - .clickable(onClick = { uriHandler.openUri(simplexTeamUri) }) + modifier = Modifier.clickable(onClick = { + uriHandler.openUri(simplexTeamUri) + }) ) Column( @@ -56,46 +48,30 @@ fun ChatHelpView(addContact: () -> Unit, doAddContact: Boolean) { ) { Text( "To start a new chat", - color = MaterialTheme.colors.onBackground, style = MaterialTheme.typography.h2 ) Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - Text( - "Tap button", - color = MaterialTheme.colors.onBackground - ) + Text("Tap button") Icon( Icons.Outlined.PersonAdd, "Add Contact", - modifier = if (doAddContact) Modifier.clickable(onClick = addContact) else Modifier, - tint = MaterialTheme.colors.onBackground, - ) - Text( - "above, then:", - color = MaterialTheme.colors.onBackground + modifier = if (addContact != null) Modifier.clickable(onClick = addContact) else Modifier, ) + Text("above, then:") } Text( buildAnnotatedString { - withStyle(SpanStyle(color = MaterialTheme.colors.onBackground, fontWeight = FontWeight.Bold)) { - append("Add new contact") - } - withStyle(SpanStyle(color = MaterialTheme.colors.onBackground)) { - append(": to create your one-time QR Code for your contact.") - } + withStyle(bold) { append("Add new contact") } + append(": to create your one-time QR Code for your contact.") } ) Text( buildAnnotatedString { - withStyle(SpanStyle(color = MaterialTheme.colors.onBackground, fontWeight = FontWeight.Bold)) { - append("Scan QR code") - } - withStyle(SpanStyle(color = MaterialTheme.colors.onBackground)) { - append(": to connect to your contact who shows QR code to you.") - } + withStyle(bold) { append("Scan QR code") } + append(": to connect to your contact who shows QR code to you.") } ) } @@ -105,51 +81,22 @@ fun ChatHelpView(addContact: () -> Unit, doAddContact: Boolean) { horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(10.dp) ) { - Text( - "To connect via link", - color = MaterialTheme.colors.onBackground, - style = MaterialTheme.typography.h2 - ) - Text( - "If you received SimpleX Chat invitation link you can open it in your browser:", - color = MaterialTheme.colors.onBackground - ) + 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 { - withStyle(SpanStyle(color = MaterialTheme.colors.onBackground)) { - append("\uD83D\uDCBB desktop: scan displayed QR code from the app, via ") - } - withStyle( - SpanStyle(color = MaterialTheme.colors.onBackground, fontWeight = FontWeight.Bold) - ) { - append("Scan QR code") - } - withStyle(SpanStyle(color = MaterialTheme.colors.onBackground)) { - append(".") - } + append("\uD83D\uDCBB desktop: scan displayed QR code from the app, via ") + withStyle(bold) { append("Scan QR code") } + append(".") } ) Text( buildAnnotatedString { - withStyle(SpanStyle(color = MaterialTheme.colors.onBackground)) { - append("\uD83D\uDCF1 mobile: tap ") - } - withStyle( - SpanStyle(color = MaterialTheme.colors.onBackground, fontWeight = FontWeight.Bold) - ) { - append("Open in mobile app") - } - withStyle(SpanStyle(color = MaterialTheme.colors.onBackground)) { - append(", then tap ") - } - withStyle( - SpanStyle(color = MaterialTheme.colors.onBackground, fontWeight = FontWeight.Bold) - ) { - append("Connect") - } - withStyle(SpanStyle(color = MaterialTheme.colors.onBackground)) { - append(" in the app.") - } + append("\uD83D\uDCF1 mobile: tap ") + withStyle(bold) { append("Open in mobile app") } + append(", then tap ") + withStyle(bold) { append("Connect") } + append(" in the app.") } ) } @@ -165,6 +112,6 @@ fun ChatHelpView(addContact: () -> Unit, doAddContact: Boolean) { @Composable fun PreviewChatHelpLayout() { SimpleXTheme { - ChatHelpView({}, false) + ChatHelpView({}) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt index d13f2863ad..fa25259b37 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt @@ -9,50 +9,39 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.ExperimentalTextApi import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import chat.simplex.app.Pages + 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.coroutines.DelicateCoroutinesApi import kotlinx.datetime.Clock -@ExperimentalTextApi @Composable -fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel, nav: NavController) { - ChatListNavLink( +fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { + ChatListNavLinkLayout( chat = chat, - action = { - when (chat.chatInfo) { - is ChatInfo.Direct -> chatNavLink(chat, chatModel, nav) - is ChatInfo.Group -> chatNavLink(chat, chatModel, nav) - is ChatInfo.ContactRequest -> contactRequestNavLink(chat.chatInfo, chatModel, nav) + click = { + if (chat.chatInfo is ChatInfo.ContactRequest) { + contactRequestAlertDialog(chat.chatInfo, chatModel) + } else { + withApi { openChat(chatModel, chat.chatInfo) } } } ) } -@DelicateCoroutinesApi -fun chatNavLink(chatPreview: Chat, chatModel: ChatModel, navController: NavController) { - withApi { - val chatInfo = chatPreview.chatInfo - val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId) - if (chat != null) { - chatModel.chatId.value = chatInfo.id - chatModel.chatItems = chat.chatItems.toMutableStateList() - navController.navigate(Pages.Chat.route) - } else { - // TODO show error? or will apiGetChat show it - } +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 } } -@DelicateCoroutinesApi -fun contactRequestNavLink(contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel, navController: NavController) { - chatModel.alertManager.showAlertDialog( +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", @@ -75,27 +64,12 @@ fun contactRequestNavLink(contactRequest: ChatInfo.ContactRequest, chatModel: Ch ) } -@ExperimentalTextApi @Composable -fun ChatListNavLink(chat: Chat, action: () -> Unit) { - ChatListNavLinkLayout( - content = { - when (chat.chatInfo) { - is ChatInfo.Direct -> ChatPreviewView(chat) - is ChatInfo.Group -> ChatPreviewView(chat) - is ChatInfo.ContactRequest -> ContactRequestView(chat) - } - }, - action = action - ) -} - -@Composable -fun ChatListNavLinkLayout(content: (@Composable () -> Unit), action: () -> Unit) { +fun ChatListNavLinkLayout(chat: Chat, click: () -> Unit) { Surface( modifier = Modifier .fillMaxWidth() - .clickable(onClick = action) + .clickable(onClick = click) .height(88.dp) ) { Row( @@ -104,18 +78,18 @@ fun ChatListNavLinkLayout(content: (@Composable () -> Unit), action: () -> Unit) .padding(vertical = 8.dp) .padding(start = 8.dp) .padding(end = 12.dp), - verticalAlignment = Alignment.Top, -// TODO? -// verticalAlignment = Alignment.CenterVertically, -// horizontalArrangement = Arrangement.SpaceEvenly + verticalAlignment = Alignment.Top ) { - content.invoke() + if (chat.chatInfo is ChatInfo.ContactRequest) { + ContactRequestView(chat) + } else { + ChatPreviewView(chat) + } } } Divider(Modifier.padding(horizontal = 8.dp)) } -@ExperimentalTextApi @Preview @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES, @@ -125,7 +99,7 @@ fun ChatListNavLinkLayout(content: (@Composable () -> Unit), action: () -> Unit) @Composable fun PreviewChatListNavLinkDirect() { SimpleXTheme { - ChatListNavLink( + ChatListNavLinkLayout( chat = Chat( chatInfo = ChatInfo.Direct.sampleData, chatItems = listOf( @@ -138,12 +112,11 @@ fun PreviewChatListNavLinkDirect() { ), chatStats = Chat.ChatStats() ), - action = {} + click = {} ) } } -@ExperimentalTextApi @Preview @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES, @@ -153,7 +126,7 @@ fun PreviewChatListNavLinkDirect() { @Composable fun PreviewChatListNavLinkGroup() { SimpleXTheme { - ChatListNavLink( + ChatListNavLinkLayout( chat = Chat( chatInfo = ChatInfo.Group.sampleData, chatItems = listOf( @@ -166,12 +139,11 @@ fun PreviewChatListNavLinkGroup() { ), chatStats = Chat.ChatStats() ), - action = {} + click = {} ) } } -@ExperimentalTextApi @Preview @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES, @@ -181,13 +153,13 @@ fun PreviewChatListNavLinkGroup() { @Composable fun PreviewChatListNavLinkContactRequest() { SimpleXTheme { - ChatListNavLink( + ChatListNavLinkLayout( chat = Chat( chatInfo = ChatInfo.ContactRequest.sampleData, chatItems = listOf(), chatStats = Chat.ChatStats() ), - action = {} + click = {} ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt index 179ed1919e..65b216fdee 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt @@ -8,24 +8,20 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.PersonAdd -import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.ExperimentalTextApi import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import chat.simplex.app.model.ChatModel import chat.simplex.app.views.chat.ChatHelpView import chat.simplex.app.views.newchat.NewChatSheet import chat.simplex.app.views.usersettings.SettingsView -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch -@ExperimentalMaterialApi class ScaffoldController(val scope: CoroutineScope) { lateinit var state: BottomSheetScaffoldState val expanded = mutableStateOf(false) @@ -41,7 +37,7 @@ class ScaffoldController(val scope: CoroutineScope) { } fun toggleSheet() { - if (state.bottomSheetState.isExpanded ?: false) collapse() else expand() + if (state.bottomSheetState.isExpanded) collapse() else expand() } fun toggleDrawer() = scope.launch { @@ -49,7 +45,6 @@ class ScaffoldController(val scope: CoroutineScope) { } } -@ExperimentalMaterialApi @Composable fun scaffoldController(): ScaffoldController { val ctrl = ScaffoldController(scope = rememberCoroutineScope()) @@ -64,36 +59,28 @@ fun scaffoldController(): ScaffoldController { return ctrl } -@ExperimentalTextApi -@DelicateCoroutinesApi -@ExperimentalPermissionsApi -@ExperimentalMaterialApi @Composable -fun ChatListView(chatModel: ChatModel, nav: NavController) { +fun ChatListView(chatModel: ChatModel) { val scaffoldCtrl = scaffoldController() BottomSheetScaffold( scaffoldState = scaffoldCtrl.state, - drawerContent = { SettingsView(chatModel, nav) }, + drawerContent = { SettingsView(chatModel) }, sheetPeekHeight = 0.dp, - sheetContent = { NewChatSheet(chatModel, scaffoldCtrl, nav) }, + sheetContent = { NewChatSheet(chatModel, scaffoldCtrl) }, sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp), ) { Box { Column( modifier = Modifier - .padding(vertical = 8.dp) .fillMaxSize() .background(MaterialTheme.colors.background) ) { ChatListToolbar(scaffoldCtrl) - when (chatModel.chatsLoaded.value) { - true -> if (chatModel.chats.isNotEmpty()) { - ChatList(chatModel, nav) - } else { - val user = chatModel.currentUser.value - Help(scaffoldCtrl, displayName = user?.profile?.displayName) - } - else -> ChatList(chatModel, nav) + if (chatModel.chats.isNotEmpty()) { + ChatList(chatModel) + } else { + val user = chatModel.currentUser.value + Help(scaffoldCtrl, displayName = user?.profile?.displayName) } } if (scaffoldCtrl.expanded.value) { @@ -108,7 +95,6 @@ fun ChatListView(chatModel: ChatModel, nav: NavController) { } } -@ExperimentalMaterialApi @Composable fun Help(scaffoldCtrl: ScaffoldController, displayName: String?) { Column( @@ -122,7 +108,7 @@ fun Help(scaffoldCtrl: ScaffoldController, displayName: String?) { style = MaterialTheme.typography.h1, color = MaterialTheme.colors.onBackground ) - ChatHelpView({ scaffoldCtrl.toggleSheet() }, true) + ChatHelpView({ scaffoldCtrl.toggleSheet() }) Row( Modifier.padding(top = 30.dp), verticalAlignment = Alignment.CenterVertically, @@ -142,7 +128,6 @@ fun Help(scaffoldCtrl: ScaffoldController, displayName: String?) { } } -@ExperimentalMaterialApi @Composable fun ChatListToolbar(scaffoldCtrl: ScaffoldController) { Row( @@ -155,7 +140,7 @@ fun ChatListToolbar(scaffoldCtrl: ScaffoldController) { ) { IconButton(onClick = { scaffoldCtrl.toggleDrawer() }) { Icon( - Icons.Outlined.Settings, + Icons.Outlined.Menu, "Settings", tint = MaterialTheme.colors.primary, modifier = Modifier.padding(10.dp) @@ -178,28 +163,14 @@ fun ChatListToolbar(scaffoldCtrl: ScaffoldController) { } } -@ExperimentalTextApi -@DelicateCoroutinesApi @Composable -fun ChatList(chatModel: ChatModel, navController: NavController) { +fun ChatList(chatModel: ChatModel) { Divider(Modifier.padding(horizontal = 8.dp)) LazyColumn( modifier = Modifier.fillMaxWidth() ) { items(chatModel.chats) { chat -> - ChatListNavLinkView(chat, chatModel, navController) + ChatListNavLinkView(chat, chatModel) } } } -//@Preview -//@Composable -//fun PreviewChatListView() { -// SimpleXTheme { -// ChatListView( -// chats = listOf( -// Chat() -// ), -// -// ) -// } -//} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt index caad7bdb1d..95fc05f7c2 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt @@ -9,7 +9,6 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.ExperimentalTextApi import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -23,7 +22,6 @@ import chat.simplex.app.views.chat.item.MarkdownText import chat.simplex.app.views.helpers.ChatInfoImage import chat.simplex.app.views.helpers.badgeLayout -@ExperimentalTextApi @Composable fun ChatPreviewView(chat: Chat) { Row { @@ -78,7 +76,6 @@ fun ChatPreviewView(chat: Chat) { } } -@ExperimentalTextApi @Preview(showBackground = true) @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt new file mode 100644 index 0000000000..f431e68e67 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt @@ -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(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() + } +} \ No newline at end of file diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ChatInfoImage.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ChatInfoImage.kt index 27eac8842b..cdc1f86039 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ChatInfoImage.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ChatInfoImage.kt @@ -1,7 +1,6 @@ package chat.simplex.app.views.helpers import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme 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.runtime.Composable 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.unit.Dp import androidx.compose.ui.unit.dp diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt new file mode 100644 index 0000000000..bb10bd639f --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt @@ -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() + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt index a8f5ec9dd4..8f148fbd3b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt @@ -2,6 +2,5 @@ package chat.simplex.app.views.helpers import kotlinx.coroutines.* -@DelicateCoroutinesApi fun withApi(action: suspend CoroutineScope.() -> Unit): Job = GlobalScope.launch { withContext(Dispatchers.Main, action) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt index f221c97567..be27d1d6e0 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt @@ -1,7 +1,6 @@ package chat.simplex.app.views.newchat import android.content.res.Configuration -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -16,60 +15,46 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController 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.CloseSheetBar import chat.simplex.app.views.helpers.shareText @Composable -fun AddContactView(chatModel: ChatModel, nav: NavController) { +fun AddContactView(chatModel: ChatModel) { val connReq = chatModel.connReqInvitation if (connReq != null) { val cxt = LocalContext.current AddContactLayout( connReq = connReq, - close = { nav.popBackStack() }, share = { shareText(cxt, connReq) } ) } } @Composable -fun AddContactLayout(connReq: String, close: () -> Unit, share: () -> Unit) { +fun AddContactLayout(connReq: String, share: () -> Unit) { Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colors.background) - .padding(horizontal = 8.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp) ) { - CloseSheetBar(close) Text( "Add contact", style = MaterialTheme.typography.h1, - color = MaterialTheme.colors.onBackground ) Text( "Show QR code to your contact\nto scan from the app", style = MaterialTheme.typography.h2, textAlign = TextAlign.Center, - color = MaterialTheme.colors.onBackground ) QRCode(connReq) Text( buildAnnotatedString { - withStyle(SpanStyle(color = MaterialTheme.colors.onBackground)) { - append("If you cannot meet in person, you can ") - } - withStyle(SpanStyle(color = MaterialTheme.colors.onBackground, fontWeight = FontWeight.Bold)) { + append("If you cannot meet in person, you can ") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { append("scan QR code in the video call") } - withStyle(SpanStyle(color = MaterialTheme.colors.onBackground)) { - 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, @@ -92,7 +77,6 @@ fun PreviewAddContactView() { SimpleXTheme { AddContactLayout( 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 = {} ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ConnectContactView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ConnectContactView.kt index 5a82ea51c9..4abcf2bc46 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ConnectContactView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ConnectContactView.kt @@ -2,7 +2,7 @@ package chat.simplex.app.views.newchat import android.content.res.Configuration import android.net.Uri -import androidx.compose.foundation.background +import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable @@ -13,47 +13,41 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import chat.simplex.app.model.ChatModel 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 kotlinx.coroutines.DelicateCoroutinesApi -@DelicateCoroutinesApi @Composable -fun ConnectContactView(chatModel: ChatModel, nav: NavController) { +fun ConnectContactView(chatModel: ChatModel, close: () -> Unit) { + BackHandler(onBack = close) ConnectContactLayout( qrCodeScanner = { QRCodeScanner { connReqUri -> try { val uri = Uri.parse(connReqUri) - withUriAction(chatModel, uri) { action -> + withUriAction(uri) { action -> connectViaUri(chatModel, action, uri) } } catch (e: RuntimeException) { - chatModel.alertManager.showAlertMsg( + AlertManager.shared.showAlertMsg( title = "Invalid QR code", text = "This QR code is not a link!" ) } - nav.popBackStack() + close() } }, - close = { nav.popBackStack() } + close = close ) } -@DelicateCoroutinesApi -fun withUriAction( - chatModel: ChatModel, uri: Uri, - run: suspend (String) -> Unit -) { +fun withUriAction(uri: Uri, run: suspend (String) -> Unit) { val action = uri.path?.drop(1) if (action == "contact" || action == "invitation") { withApi { run(action) } } else { - chatModel.alertManager.showAlertMsg( + AlertManager.shared.showAlertMsg( title = "Invalid link!", text = "This link is not a valid connection link!" ) @@ -66,7 +60,7 @@ suspend fun connectViaUri(chatModel: ChatModel, action: String, uri: Uri) { val whenConnected = if (action == "contact") "your connection request is accepted" else "your contact's device is online" - chatModel.alertManager.showAlertMsg( + AlertManager.shared.showAlertMsg( title = "Connection request sent!", text = "You will be connected when $whenConnected, please wait or check later!" ) @@ -75,50 +69,41 @@ suspend fun connectViaUri(chatModel: ChatModel, action: String, uri: Uri) { @Composable fun ConnectContactLayout(qrCodeScanner: @Composable () -> Unit, close: () -> Unit) { - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colors.background) - .padding(horizontal = 8.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - CloseSheetBar(close) - Text( - "Scan QR code", - style = MaterialTheme.typography.h1, - color = MaterialTheme.colors.onBackground - ) - Text( - "Your chat profile will be sent\nto your contact", - style = MaterialTheme.typography.h2, - textAlign = TextAlign.Center, - color = MaterialTheme.colors.onBackground, - modifier = Modifier.padding(bottom = 4.dp) - ) - Box( - Modifier - .fillMaxWidth() - .aspectRatio(ratio = 1F) - ) { qrCodeScanner() } - Text( - buildAnnotatedString { - withStyle(SpanStyle(color = MaterialTheme.colors.onBackground)) { + ModalView(close) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + "Scan QR code", + style = MaterialTheme.typography.h1, + ) + Text( + "Your chat profile will be sent\nto your contact", + style = MaterialTheme.typography.h2, + textAlign = TextAlign.Center, + modifier = Modifier.padding(bottom = 4.dp) + ) + Box( + Modifier + .fillMaxWidth() + .aspectRatio(ratio = 1F) + ) { qrCodeScanner() } + Text( + buildAnnotatedString { append("If you cannot meet in person, you can ") - } - withStyle(SpanStyle(color = MaterialTheme.colors.onBackground, fontWeight = FontWeight.Bold)) { - append("scan QR code in the video call") - } - withStyle(SpanStyle(color = MaterialTheme.colors.onBackground)) { + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append("scan QR code in the video call") + } append(", or you can create the invitation link.") - } - }, - textAlign = TextAlign.Center, - style = MaterialTheme.typography.caption, - modifier = Modifier - .padding(horizontal = 16.dp) - .padding(top = 4.dp) - ) + }, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.caption, + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 4.dp) + ) + } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt index 64db15a0cf..65d882ae71 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt @@ -14,22 +14,15 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview 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.ui.theme.HighOrLowlight import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.chatlist.ScaffoldController import chat.simplex.app.views.helpers.withApi -import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberPermissionState -import kotlinx.coroutines.DelicateCoroutinesApi -@DelicateCoroutinesApi -@ExperimentalPermissionsApi -@ExperimentalMaterialApi @Composable -fun NewChatSheet(chatModel: ChatModel, newChatCtrl: ScaffoldController, nav: NavController) { +fun NewChatSheet(chatModel: ChatModel, newChatCtrl: ScaffoldController) { val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) NewChatSheetLayout( addContact = { @@ -39,41 +32,48 @@ fun NewChatSheet(chatModel: ChatModel, newChatCtrl: ScaffoldController, nav: Nav // hide spinner if (chatModel.connReqInvitation != null) { newChatCtrl.collapse() - nav.navigate(Pages.AddContact.route) + ModalManager.shared.showModal { AddContactView(chatModel) } } } }, scanCode = { newChatCtrl.collapse() - nav.navigate(Pages.Connect.route) + ModalManager.shared.showCustomModal { close -> ConnectContactView(chatModel, close) } cameraPermissionState.launchPermissionRequest() - }, - close = { - newChatCtrl.collapse() } ) } @Composable -fun NewChatSheetLayout(addContact: () -> Unit, scanCode: () -> Unit, close: () -> Unit) { - Row(Modifier +fun NewChatSheetLayout(addContact: () -> Unit, scanCode: () -> Unit) { + Row( + Modifier .fillMaxWidth() .padding(horizontal = 8.dp, vertical = 48.dp), horizontalArrangement = Arrangement.SpaceEvenly ) { - Box(Modifier.weight(1F).fillMaxWidth()) { + Box( + Modifier + .weight(1F) + .fillMaxWidth()) { ActionButton( "Add contact", "(create QR code\nor link)", Icons.Outlined.PersonAdd, click = addContact ) } - Box(Modifier.weight(1F).fillMaxWidth()) { + Box( + Modifier + .weight(1F) + .fillMaxWidth()) { ActionButton( "Scan QR code", "(in person or in video call)", Icons.Outlined.QrCode, click = scanCode ) } - Box(Modifier.weight(1F).fillMaxWidth()) { + Box( + Modifier + .weight(1F) + .fillMaxWidth()) { ActionButton( "Create Group", "(coming soon!)", Icons.Outlined.GroupAdd, disabled = true @@ -116,8 +116,7 @@ fun PreviewNewChatSheet() { SimpleXTheme { NewChatSheetLayout( addContact = {}, - scanCode = {}, - close = {}, + scanCode = {} ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCodeScanner.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCodeScanner.kt index 8f2b940258..f6bee70490 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCodeScanner.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCodeScanner.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat +import chat.simplex.app.TAG import com.google.common.util.concurrent.ListenableFuture import com.google.mlkit.vision.barcode.BarcodeScannerOptions import com.google.mlkit.vision.barcode.BarcodeScanning @@ -62,7 +63,7 @@ fun QRCodeScanner(onBarcode: (String) -> Unit) { cameraProvider.unbindAll() cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis) } catch (e: Exception) { - Log.d("SIMPLEX", "CameraPreview: ${e.localizedMessage}") + Log.d(TAG, "CameraPreview: ${e.localizedMessage}") } }, ContextCompat.getMainExecutor(context)) } @@ -90,11 +91,11 @@ class BarCodeAnalyser( if (barcodes.isNotEmpty()) { onBarcodeDetected(barcodes) } else { - Log.d("SIMPLEX", "BarcodeAnalyser: No barcode Scanned") + Log.d(TAG, "BarcodeAnalyser: No barcode Scanned") } } .addOnFailureListener { exception -> - Log.d("SIMPLEX", "BarcodeAnalyser: Something went wrong $exception") + Log.e(TAG, "BarcodeAnalyser: Something went wrong $exception") } .addOnCompleteListener { image.close() diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/HelpView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/HelpView.kt index fcc225912e..9d7d380c7a 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/HelpView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/HelpView.kt @@ -1,8 +1,8 @@ package chat.simplex.app.views.usersettings import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +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 @@ -10,40 +10,27 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import chat.simplex.app.model.ChatModel import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.chat.ChatHelpView -import chat.simplex.app.views.helpers.CloseSheetBar @Composable -fun HelpView(chatModel: ChatModel, nav: NavController) { +fun HelpView(chatModel: ChatModel) { val user = chatModel.currentUser.value if (user != null) { - HelpLayout( - displayName = user.profile.displayName, - back = nav::popBackStack - ) + HelpLayout(displayName = user.profile.displayName) } } @Composable -fun HelpLayout(displayName: String, back: () -> Unit) { - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colors.background) - .padding(horizontal = 8.dp), - horizontalAlignment = Alignment.Start - ) { - CloseSheetBar(back) +fun HelpLayout(displayName: String) { + Column(horizontalAlignment = Alignment.Start) { Text( "Welcome $displayName!", Modifier.padding(bottom = 24.dp), style = MaterialTheme.typography.h1, - color = MaterialTheme.colors.onBackground ) - ChatHelpView({}, false) + ChatHelpView() } } @@ -56,9 +43,6 @@ fun HelpLayout(displayName: String, back: () -> Unit) { @Composable fun PreviewHelpView() { SimpleXTheme { - HelpLayout( - displayName = "Alice", - back = {} - ) + HelpLayout(displayName = "Alice") } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/MarkdownHelpView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/MarkdownHelpView.kt index d7e647e042..0f0a649f1e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/MarkdownHelpView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/MarkdownHelpView.kt @@ -1,72 +1,53 @@ package chat.simplex.app.views.usersettings import android.content.res.Configuration -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.material.* +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.* -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import chat.simplex.app.model.Format import chat.simplex.app.model.FormatColor import chat.simplex.app.ui.theme.SimpleXTheme -import chat.simplex.app.views.helpers.CloseSheetBar @Composable -fun MarkdownHelpView(nav: NavController) { - MarkdownHelpLayout(nav::popBackStack) -} - -@Composable -fun MarkdownHelpLayout(back: () -> Unit) { - Surface( - Modifier - .background(MaterialTheme.colors.background) - .fillMaxSize() - ) { - Column { - CloseSheetBar(back) - 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 text", Format.Bold()) - MdFormat("_italic_", "italic text", Format.Italic()) - MdFormat("~strike~", "strikethrough text", Format.StrikeThrough()) - MdFormat("`code`", "a = b + c", Format.Snippet()) - Row { - MdSyntax("!1 colored!") - Text(buildAnnotatedString { - withStyle(Format.Colored(FormatColor.red).style) { append("red text") } - 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 text") } - }) - } - } +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") } + }) } } } @@ -105,6 +86,6 @@ fun appendColor(b: AnnotatedString.Builder, s: String, c: FormatColor, after: St @Composable fun PreviewMarkdownHelpView() { SimpleXTheme { - MarkdownHelpLayout(back = {}) + MarkdownHelpView() } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt index 9581ca62bc..b9b4e69cc5 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt @@ -17,20 +17,21 @@ import androidx.compose.ui.text.font.FontWeight 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.R import chat.simplex.app.model.ChatModel import chat.simplex.app.model.Profile import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.views.TerminalView +import chat.simplex.app.views.newchat.ModalManager @Composable -fun SettingsView(chatModel: ChatModel, nav: NavController) { +fun SettingsView(chatModel: ChatModel) { val user = chatModel.currentUser.value if (user != null) { SettingsLayout( profile = user.profile, - navigate = nav::navigate + showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } }, + showTerminal = { ModalManager.shared.showCustomModal { close -> TerminalView(chatModel, close) } } ) } } @@ -41,7 +42,8 @@ val simplexTeamUri = @Composable fun SettingsLayout( profile: Profile, - navigate: (String) -> Unit + showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + showTerminal: () -> Unit ) { val uriHandler = LocalUriHandler.current Surface( @@ -59,10 +61,11 @@ fun SettingsLayout( Text( "Your Settings", style = MaterialTheme.typography.h1, + modifier = Modifier.padding(start = 8.dp) ) Spacer(Modifier.height(30.dp)) - SettingsSectionView({ navigate(Pages.UserProfile.route) }, 60.dp) { + SettingsSectionView(showModal { UserProfileView(it) }, 60.dp) { Icon( Icons.Outlined.AccountCircle, contentDescription = "Avatar Placeholder", @@ -78,7 +81,7 @@ fun SettingsLayout( } } Divider(Modifier.padding(horizontal = 8.dp)) - SettingsSectionView({ navigate(Pages.UserAddress.route) }) { + SettingsSectionView(showModal { UserAddressView(it) }) { Icon( Icons.Outlined.QrCode, contentDescription = "Address", @@ -88,7 +91,7 @@ fun SettingsLayout( } Spacer(Modifier.height(24.dp)) - SettingsSectionView({ navigate(Pages.Help.route) }) { + SettingsSectionView(showModal { HelpView(it) }) { Icon( Icons.Outlined.HelpOutline, contentDescription = "Chat help", @@ -96,7 +99,7 @@ fun SettingsLayout( Spacer(Modifier.padding(horizontal = 4.dp)) Text("How to use SimpleX Chat") } - SettingsSectionView({ navigate(Pages.Markdown.route) }) { + SettingsSectionView(showModal { MarkdownHelpView() }) { Icon( Icons.Outlined.TextFormat, contentDescription = "Markdown help", @@ -130,7 +133,7 @@ fun SettingsLayout( } Spacer(Modifier.height(24.dp)) - SettingsSectionView({ navigate(Pages.Terminal.route) }) { + SettingsSectionView(showTerminal) { Icon( painter = painterResource(id = R.drawable.ic_outline_terminal), contentDescription = "Chat console", @@ -159,12 +162,12 @@ fun SettingsLayout( } @Composable -fun SettingsSectionView(func: () -> Unit, height: Dp = 48.dp, content: (@Composable () -> Unit)) { +fun SettingsSectionView(click: () -> Unit, height: Dp = 48.dp, content: (@Composable () -> Unit)) { Row( Modifier .padding(start = 8.dp) .fillMaxWidth() - .clickable(onClick = func) + .clickable(onClick = click) .height(height), verticalAlignment = Alignment.CenterVertically ) { @@ -183,7 +186,8 @@ fun PreviewSettingsLayout() { SimpleXTheme { SettingsLayout( profile = Profile.sampleData, - navigate = {} + showModal = {{}}, + showTerminal = {} ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserAddressView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserAddressView.kt index d082048f8d..a6a36bf16f 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserAddressView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserAddressView.kt @@ -1,7 +1,6 @@ package chat.simplex.app.views.usersettings import android.content.res.Configuration -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -14,7 +13,6 @@ 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 androidx.navigation.NavController import chat.simplex.app.model.ChatModel import chat.simplex.app.ui.theme.SimpleButton import chat.simplex.app.ui.theme.SimpleXTheme @@ -22,11 +20,10 @@ import chat.simplex.app.views.helpers.* import chat.simplex.app.views.newchat.QRCode @Composable -fun UserAddressView(chatModel: ChatModel, nav: NavController) { +fun UserAddressView(chatModel: ChatModel) { val cxt = LocalContext.current UserAddressLayout( userAddress = chatModel.userAddress.value, - back = { nav.popBackStack() }, createAddress = { withApi { chatModel.userAddress.value = chatModel.controller.apiCreateUserAddress() @@ -34,7 +31,7 @@ fun UserAddressView(chatModel: ChatModel, nav: NavController) { }, share = { userAddress: String -> shareText(cxt, userAddress) }, deleteAddress = { - chatModel.alertManager.showAlertMsg( + AlertManager.shared.showAlertMsg( title = "Delete address?", text = "All your contacts will remain connected", confirmText = "Delete", @@ -52,31 +49,23 @@ fun UserAddressView(chatModel: ChatModel, nav: NavController) { @Composable fun UserAddressLayout( userAddress: String?, - back: () -> Unit, createAddress: () -> Unit, share: (String) -> Unit, deleteAddress: () -> Unit ) { Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colors.background) - .padding(horizontal = 8.dp), horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.Top ) { - CloseSheetBar(back) Text( "Your chat address", Modifier.padding(bottom = 24.dp), style = MaterialTheme.typography.h1, - color = MaterialTheme.colors.onBackground ) 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 = 24.dp), - color = MaterialTheme.colors.onBackground ) Column( Modifier @@ -119,7 +108,6 @@ fun PreviewUserAddressLayoutNoAddress() { SimpleXTheme { UserAddressLayout( userAddress = null, - back = {}, createAddress = {}, share = { _ -> }, deleteAddress = {}, @@ -138,7 +126,6 @@ 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", - back = {}, createAddress = {}, share = { _ -> }, deleteAddress = {}, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt index 6af1f44db1..4dd2ad0d44 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt @@ -1,7 +1,6 @@ package chat.simplex.app.views.usersettings import android.content.res.Configuration -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* 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.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import chat.simplex.app.model.ChatModel import chat.simplex.app.model.Profile import chat.simplex.app.ui.theme.SimpleXTheme -import chat.simplex.app.views.helpers.CloseSheetBar import chat.simplex.app.views.helpers.withApi @Composable -fun UserProfileView(chatModel: ChatModel, nav: NavController) { +fun UserProfileView(chatModel: ChatModel) { val user = chatModel.currentUser.value if (user != null) { var editProfile by remember { mutableStateOf(false) } @@ -31,7 +28,6 @@ fun UserProfileView(chatModel: ChatModel, nav: NavController) { UserProfileLayout( editProfile = editProfile, profile = profile, - back = { nav.popBackStack() }, editProfileOff = { editProfile = false }, editProfileOn = { editProfile = true }, saveProfile = { displayName: String, fullName: String -> @@ -54,19 +50,11 @@ fun UserProfileView(chatModel: ChatModel, nav: NavController) { fun UserProfileLayout( editProfile: Boolean, profile: Profile, - back: () -> Unit, editProfileOff: () -> Unit, editProfileOn: () -> Unit, saveProfile: (String, String) -> Unit, ) { - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colors.background) - .padding(horizontal = 8.dp), - horizontalAlignment = Alignment.Start - ) { - CloseSheetBar(back) + Column(horizontalAlignment = Alignment.Start) { Text( "Your chat profile", Modifier.padding(bottom = 24.dp), @@ -185,7 +173,6 @@ fun PreviewUserProfileLayoutEditOff() { UserProfileLayout( profile = Profile.sampleData, editProfile = false, - back = {}, editProfileOff = {}, editProfileOn = {}, saveProfile = { _, _ -> } @@ -205,7 +192,6 @@ fun PreviewUserProfileLayoutEditOn() { UserProfileLayout( profile = Profile.sampleData, editProfile = true, - back = {}, editProfileOff = {}, editProfileOn = {}, saveProfile = { _, _ -> } diff --git a/apps/android/app/src/main/res/drawable-hdpi/ntf_icon.png b/apps/android/app/src/main/res/drawable-hdpi/ntf_icon.png new file mode 100644 index 0000000000..e17716d593 Binary files /dev/null and b/apps/android/app/src/main/res/drawable-hdpi/ntf_icon.png differ diff --git a/apps/android/app/src/main/res/drawable-mdpi/ntf_icon.png b/apps/android/app/src/main/res/drawable-mdpi/ntf_icon.png new file mode 100644 index 0000000000..4f9c5cf450 Binary files /dev/null and b/apps/android/app/src/main/res/drawable-mdpi/ntf_icon.png differ diff --git a/apps/android/app/src/main/res/drawable-xhdpi/ntf_icon.png b/apps/android/app/src/main/res/drawable-xhdpi/ntf_icon.png new file mode 100644 index 0000000000..44f7315eec Binary files /dev/null and b/apps/android/app/src/main/res/drawable-xhdpi/ntf_icon.png differ diff --git a/apps/android/app/src/main/res/drawable-xxhdpi/ntf_icon.png b/apps/android/app/src/main/res/drawable-xxhdpi/ntf_icon.png new file mode 100644 index 0000000000..01942e7864 Binary files /dev/null and b/apps/android/app/src/main/res/drawable-xxhdpi/ntf_icon.png differ diff --git a/apps/android/app/src/main/res/drawable-xxxhdpi/ntf_icon.png b/apps/android/app/src/main/res/drawable-xxxhdpi/ntf_icon.png new file mode 100644 index 0000000000..71e75ec569 Binary files /dev/null and b/apps/android/app/src/main/res/drawable-xxxhdpi/ntf_icon.png differ diff --git a/apps/android/build.gradle b/apps/android/build.gradle index 6cb503be83..af369aaf9a 100644 --- a/apps/android/build.gradle +++ b/apps/android/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } 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-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. plugins { - id 'com.android.application' version '7.1.1' apply false - id 'com.android.library' version '7.1.1' apply false + id 'com.android.application' version '7.1.2' 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.plugin.serialization' version '1.6.10' } diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index 36f233972c..3d38c4c7ec 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -13,32 +13,30 @@ struct ContentView: View { @State private var showNotificationAlert = false var body: some View { - ZStack { - if let user = chatModel.currentUser { - ChatListView(user: user) - .onAppear { - do { - try apiStartChat() - chatModel.chats = try apiGetChats() - } catch { - fatalError("Failed to start or load chats: \(error)") - } - ChatReceiver.shared.start() - NtfManager.shared.requestAuthorization(onDeny: { - alertManager.showAlert(notificationAlert()) - }) + if let user = chatModel.currentUser { + ChatListView(user: user) + .onAppear { + do { + try apiStartChat() + chatModel.chats = try apiGetChats() + } catch { + fatalError("Failed to start or load chats: \(error)") } - } else { - WelcomeView() - } + ChatReceiver.shared.start() + NtfManager.shared.requestAuthorization(onDeny: { + alertManager.showAlert(notificationAlert()) + }) + } + .alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! } + } else { + WelcomeView() } - .alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! } } func notificationAlert() -> Alert { Alert( 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")) { DispatchQueue.main.async { UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index c8d37b3b65..3361c9b710 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -22,6 +22,9 @@ final class ChatModel: ObservableObject { @Published var terminalItems: [TerminalItem] = [] @Published var userAddress: String? @Published var appOpenUrl: URL? + + var messageDelivery: Dictionary Void> = [:] + static let shared = ChatModel() func hasChat(_ id: String) -> Bool { @@ -622,15 +625,14 @@ struct RcvFileTransfer: Decodable { enum MsgContent { case text(String) + // TODO include original JSON, possibly using https://github.com/zoul/generic-json-swift case unknown(type: String, text: String) - case invalid(error: String) var text: String { get { switch self { case let .text(text): return text case let .unknown(_, text): return text - case .invalid: return "invalid" } } } @@ -652,8 +654,8 @@ enum MsgContent { extension MsgContent: Decodable { init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) do { + let container = try decoder.container(keyedBy: CodingKeys.self) let type = try container.decode(String.self, forKey: CodingKeys.type) switch type { case "text": @@ -664,7 +666,7 @@ extension MsgContent: Decodable { self = .unknown(type: type, text: text ?? "unknown message format") } } catch { - self = .invalid(error: String(describing: error)) + self = .unknown(type: "unknown", text: "invalid message format") } } } diff --git a/apps/ios/Shared/Model/NtfManager.swift b/apps/ios/Shared/Model/NtfManager.swift index 5459d9b673..3f0bbca448 100644 --- a/apps/ios/Shared/Model/NtfManager.swift +++ b/apps/ios/Shared/Model/NtfManager.swift @@ -178,6 +178,7 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { if let s = body { content.body = s } content.targetContentIdentifier = targetContentIdentifier content.userInfo = userInfo + // TODO move logic of adding sound here, so it applies to background notifications too content.sound = .default // content.interruptionLevel = .active // content.relevanceScore = 0.5 // 0-1 diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 5717791bb6..a6a45a8f1c 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -95,6 +95,7 @@ enum ChatResponse: Decodable, Error { case response(type: String, json: String) case activeUser(user: User) case chatStarted + case chatRunning case apiChats(chats: [ChatData]) case apiChat(chat: ChatData) case invitation(connReqInvitation: String) @@ -131,6 +132,7 @@ enum ChatResponse: Decodable, Error { case let .response(type, _): return "* \(type)" case .activeUser: return "activeUser" case .chatStarted: return "chatStarted" + case .chatRunning: return "chatRunning" case .apiChats: return "apiChats" case .apiChat: return "apiChat" case .invitation: return "invitation" @@ -170,6 +172,7 @@ enum ChatResponse: Decodable, Error { case let .response(_, json): return json case let .activeUser(user): return String(describing: user) case .chatStarted: return noDetails + case .chatRunning: return noDetails case let .apiChats(chats): return String(describing: chats) case let .apiChat(chat): return String(describing: chat) case let .invitation(connReqInvitation): return connReqInvitation @@ -238,10 +241,53 @@ enum TerminalItem: Identifiable { } } -func chatSendCmdSync(_ cmd: ChatCommand) -> ChatResponse { +private func _sendCmd(_ cmd: ChatCommand) -> ChatResponse { 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)") - 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)") if case let .response(_, json) = resp { logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)") @@ -253,16 +299,19 @@ func chatSendCmdSync(_ cmd: ChatCommand) -> ChatResponse { return resp } -func chatSendCmd(_ cmd: ChatCommand) async -> ChatResponse { +func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil) async -> ChatResponse { await withCheckedContinuation { cont in - cont.resume(returning: chatSendCmdSync(cmd)) + cont.resume(returning: chatSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay)) } } func chatRecvMsg() async -> ChatResponse { await withCheckedContinuation { cont in - let resp = chatResponse(chat_recv_msg(getChatCtrl())!) - cont.resume(returning: resp) + _ = withBGTask(bgDelay: msgDelay) { + let resp = chatResponse(chat_recv_msg(getChatCtrl())!) + cont.resume(returning: resp) + return resp + } } } @@ -301,13 +350,30 @@ func apiGetChat(type: ChatType, id: Int64) async throws -> Chat { } func apiSendMessage(type: ChatType, id: Int64, msg: MsgContent) async throws -> ChatItem { - let r = await chatSendCmd(.apiSendMessage(type: type, id: id, msg: msg)) - if case let .newChatItem(aChatItem) = r { return aChatItem.chatItem } + let chatModel = ChatModel.shared + 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 apiAddContact() async throws -> String { - let r = await chatSendCmd(.addContact) +func apiAddContact() throws -> String { + let r = chatSendCmdSync(.addContact, bgTask: false) if case let .invitation(connReqInvitation) = r { return connReqInvitation } throw r } @@ -322,7 +388,7 @@ func apiConnect(connReq: String) async throws { } func apiDeleteChat(type: ChatType, id: Int64) async throws { - let r = await chatSendCmd(.apiDeleteChat(type: type, id: id)) + let r = await chatSendCmd(.apiDeleteChat(type: type, id: id), bgTask: false) if case .contactDeleted = r { return } throw r } @@ -447,6 +513,8 @@ class ChatReceiver { self._lastMsgTime = .now 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() } } @@ -505,6 +573,13 @@ func processReceivedMsg(_ res: ChatResponse) { let cItem = aChatItem.chatItem if chatModel.upsertChatItem(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: logger.debug("unsupported event: \(res.responseType)") @@ -574,7 +649,10 @@ private func getChatCtrl() -> chat_ctrl { if let controller = chatController { return controller } let dataDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.path + "/mobile_v1" var cstr = dataDir.cString(using: .utf8)! + logger.debug("getChatCtrl: chat_init") + ChatModel.shared.terminalItems.append(.cmd(.now, .string("chat_init"))) chatController = chat_init(&cstr) + ChatModel.shared.terminalItems.append(.resp(.now, .response(type: "chat_controller", json: "chat_controller: no details"))) return chatController! } diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index fe1e24ea39..766784b0e8 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -84,7 +84,7 @@ struct ChatListView: View { let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)") return Alert( 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")) { DispatchQueue.main.async { Task { diff --git a/apps/ios/Shared/Views/NewChat/AddContactView.swift b/apps/ios/Shared/Views/NewChat/AddContactView.swift index f6d62deb26..3c924697ec 100644 --- a/apps/ios/Shared/Views/NewChat/AddContactView.swift +++ b/apps/ios/Shared/Views/NewChat/AddContactView.swift @@ -22,7 +22,9 @@ struct AddContactView: View { .multilineTextAlignment(.center) QRCode(uri: connReqInvitation) .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) .multilineTextAlignment(.center) .padding(.horizontal) diff --git a/apps/ios/Shared/Views/NewChat/NewChatButton.swift b/apps/ios/Shared/Views/NewChat/NewChatButton.swift index db8e1d9f39..d984ddd0df 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatButton.swift @@ -35,18 +35,20 @@ struct NewChatButton: View { } func addContactAction() { - Task { - do { - connReqInvitation = try await apiAddContact() - addContact = true - } catch { - DispatchQueue.global().async { - connectionErrorAlert(error) - } - logger.error("NewChatButton.addContactAction apiAddContact error: \(error.localizedDescription)") + do { + connReqInvitation = try apiAddContact() + addContact = true + } catch { + DispatchQueue.global().async { + connectionErrorAlert(error) } + logger.error("NewChatButton.addContactAction apiAddContact error: \(error.localizedDescription)") } } + + func addContactSheet() -> some View { + AddContactView(connReqInvitation: connReqInvitation) + } func connectContactSheet() -> some View { ConnectContactView(completed: { err in diff --git a/apps/ios/Shared/Views/UserSettings/MarkdownHelp.swift b/apps/ios/Shared/Views/UserSettings/MarkdownHelp.swift index d9a73372dd..855d0adff1 100644 --- a/apps/ios/Shared/Views/UserSettings/MarkdownHelp.swift +++ b/apps/ios/Shared/Views/UserSettings/MarkdownHelp.swift @@ -13,13 +13,13 @@ struct MarkdownHelp: View { VStack(alignment: .leading, spacing: 8) { Text("You can use markdown to format messages:") .padding(.bottom) - mdFormat("*bold*", Text("bold text").bold()) - mdFormat("_italic_", Text("italic text").italic()) - mdFormat("~strike~", Text("strikethrough text").strikethrough()) - mdFormat("`code`", Text("`a = b + c`").font(.body.monospaced())) - mdFormat("!1 colored!", Text("red text").foregroundColor(.red) + Text(" (") + color("1", .red) + color("2", .green) + color("3", .blue) + color("4", .yellow) + color("5", .cyan) + Text("6").foregroundColor(.purple) + Text(")")) + mdFormat("*bold*", Text("bold").bold()) + mdFormat("_italic_", Text("italic").italic()) + mdFormat("~strike~", Text("strike").strikethrough()) + mdFormat("`a + b`", Text("`a + b`").font(.body.monospaced())) + mdFormat("!1 colored!", Text("colored").foregroundColor(.red) + Text(" (") + color("1", .red) + color("2", .green) + color("3", .blue) + color("4", .yellow) + color("5", .cyan) + Text("6").foregroundColor(.purple) + Text(")")) ( - mdFormat("#secret#", Text("secret text") + mdFormat("#secret#", Text("secret") .foregroundColor(.clear) .underline(color: .primary) + Text(" (can be copied)")) ) diff --git a/apps/ios/Shared/Views/WelcomeView.swift b/apps/ios/Shared/Views/WelcomeView.swift index 65e356f31f..fc57cca555 100644 --- a/apps/ios/Shared/Views/WelcomeView.swift +++ b/apps/ios/Shared/Views/WelcomeView.swift @@ -34,21 +34,15 @@ struct WelcomeView: View { .padding(.bottom) ZStack(alignment: .topLeading) { if !validDisplayName(displayName) { - Button { - AlertManager.shared.showAlertMsg( - title: "Display name", - message: "Display name can't contain spaces" - ) - } label: { - Image(systemName: "exclamationmark.circle") - .foregroundColor(.red) - .padding(.top, 4) - } + Image(systemName: "exclamationmark.circle") + .foregroundColor(.red) + .padding(.top, 4) } TextField("Display name", text: $displayName) .textInputAutocapitalization(.never) .disableAutocorrection(true) .padding(.leading, 28) + .padding(.bottom, 2) } .padding(.bottom) TextField("Full name (optional)", text: $fullName) @@ -68,7 +62,7 @@ struct WelcomeView: View { fatalError("Failed to create user: \(error)") } } - .disabled(!validDisplayName(displayName)) + .disabled(!validDisplayName(displayName) || displayName == "") } } .padding() diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index dafbc7e23d..0fedf61c19 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -13,16 +13,6 @@ 5C116CDD27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; }; 5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */; }; 5C1A4C1F27A715B700EAD5AD /* ChatItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */; }; - 5C27D00827C7D8B500DD6182 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C27D00327C7D8B500DD6182 /* libgmpxx.a */; }; - 5C27D00927C7D8B500DD6182 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C27D00327C7D8B500DD6182 /* libgmpxx.a */; }; - 5C27D00A27C7D8B500DD6182 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C27D00427C7D8B500DD6182 /* libgmp.a */; }; - 5C27D00B27C7D8B500DD6182 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C27D00427C7D8B500DD6182 /* libgmp.a */; }; - 5C27D00C27C7D8B500DD6182 /* libHSsimplex-chat-1.2.1-KSWVFEZPyZUBOUtDs8BKKY.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C27D00527C7D8B500DD6182 /* libHSsimplex-chat-1.2.1-KSWVFEZPyZUBOUtDs8BKKY.a */; }; - 5C27D00D27C7D8B500DD6182 /* libHSsimplex-chat-1.2.1-KSWVFEZPyZUBOUtDs8BKKY.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C27D00527C7D8B500DD6182 /* libHSsimplex-chat-1.2.1-KSWVFEZPyZUBOUtDs8BKKY.a */; }; - 5C27D00E27C7D8B500DD6182 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C27D00627C7D8B500DD6182 /* libffi.a */; }; - 5C27D00F27C7D8B500DD6182 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C27D00627C7D8B500DD6182 /* libffi.a */; }; - 5C27D01027C7D8B500DD6182 /* libHSsimplex-chat-1.2.1-KSWVFEZPyZUBOUtDs8BKKY-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C27D00727C7D8B500DD6182 /* libHSsimplex-chat-1.2.1-KSWVFEZPyZUBOUtDs8BKKY-ghc8.10.7.a */; }; - 5C27D01127C7D8B500DD6182 /* libHSsimplex-chat-1.2.1-KSWVFEZPyZUBOUtDs8BKKY-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C27D00727C7D8B500DD6182 /* libHSsimplex-chat-1.2.1-KSWVFEZPyZUBOUtDs8BKKY-ghc8.10.7.a */; }; 5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */; }; 5C2E260827A2941F00F70299 /* SimpleXAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */; }; 5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260A27A30CFA00F70299 /* ChatListView.swift */; }; @@ -39,6 +29,16 @@ 5C5346A927B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; }; 5C577F7D27C83AA10006112D /* MarkdownHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */; }; 5C577F7E27C83AA10006112D /* MarkdownHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */; }; + 5C67D31827D0003A00E4261F /* libHSsimplex-chat-1.3.0-5IozqhzNoFs59GB71w8Qla-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C67D31327D0003A00E4261F /* libHSsimplex-chat-1.3.0-5IozqhzNoFs59GB71w8Qla-ghc8.10.7.a */; }; + 5C67D31927D0003A00E4261F /* libHSsimplex-chat-1.3.0-5IozqhzNoFs59GB71w8Qla-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C67D31327D0003A00E4261F /* libHSsimplex-chat-1.3.0-5IozqhzNoFs59GB71w8Qla-ghc8.10.7.a */; }; + 5C67D31A27D0003A00E4261F /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C67D31427D0003A00E4261F /* libffi.a */; }; + 5C67D31B27D0003A00E4261F /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C67D31427D0003A00E4261F /* libffi.a */; }; + 5C67D31C27D0003A00E4261F /* libHSsimplex-chat-1.3.0-5IozqhzNoFs59GB71w8Qla.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C67D31527D0003A00E4261F /* libHSsimplex-chat-1.3.0-5IozqhzNoFs59GB71w8Qla.a */; }; + 5C67D31D27D0003A00E4261F /* libHSsimplex-chat-1.3.0-5IozqhzNoFs59GB71w8Qla.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C67D31527D0003A00E4261F /* libHSsimplex-chat-1.3.0-5IozqhzNoFs59GB71w8Qla.a */; }; + 5C67D31E27D0003A00E4261F /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C67D31627D0003A00E4261F /* libgmpxx.a */; }; + 5C67D31F27D0003A00E4261F /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C67D31627D0003A00E4261F /* libgmpxx.a */; }; + 5C67D32027D0003A00E4261F /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C67D31727D0003A00E4261F /* libgmp.a */; }; + 5C67D32127D0003A00E4261F /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C67D31727D0003A00E4261F /* libgmp.a */; }; 5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AD81227A834E300348BD7 /* NewChatButton.swift */; }; 5C6AD81427A834E300348BD7 /* NewChatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AD81227A834E300348BD7 /* NewChatButton.swift */; }; 5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */; }; @@ -125,11 +125,6 @@ 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreviewView.swift; sourceTree = ""; }; 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestView.swift; sourceTree = ""; }; 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemView.swift; sourceTree = ""; }; - 5C27D00327C7D8B500DD6182 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 5C27D00427C7D8B500DD6182 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5C27D00527C7D8B500DD6182 /* libHSsimplex-chat-1.2.1-KSWVFEZPyZUBOUtDs8BKKY.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.2.1-KSWVFEZPyZUBOUtDs8BKKY.a"; sourceTree = ""; }; - 5C27D00627C7D8B500DD6182 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5C27D00727C7D8B500DD6182 /* libHSsimplex-chat-1.2.1-KSWVFEZPyZUBOUtDs8BKKY-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.2.1-KSWVFEZPyZUBOUtDs8BKKY-ghc8.10.7.a"; sourceTree = ""; }; 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXAPI.swift; sourceTree = ""; }; 5C2E260927A2C63500F70299 /* MyPlayground.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = MyPlayground.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 5C2E260A27A30CFA00F70299 /* ChatListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListView.swift; sourceTree = ""; }; @@ -140,6 +135,11 @@ 5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = ""; }; 5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = ""; }; 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownHelp.swift; sourceTree = ""; }; + 5C67D31327D0003A00E4261F /* libHSsimplex-chat-1.3.0-5IozqhzNoFs59GB71w8Qla-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.3.0-5IozqhzNoFs59GB71w8Qla-ghc8.10.7.a"; sourceTree = ""; }; + 5C67D31427D0003A00E4261F /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 5C67D31527D0003A00E4261F /* libHSsimplex-chat-1.3.0-5IozqhzNoFs59GB71w8Qla.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.3.0-5IozqhzNoFs59GB71w8Qla.a"; sourceTree = ""; }; + 5C67D31627D0003A00E4261F /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5C67D31727D0003A00E4261F /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 5C6AD81227A834E300348BD7 /* NewChatButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatButton.swift; sourceTree = ""; }; 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMetaView.swift; sourceTree = ""; }; 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = ""; }; @@ -186,14 +186,14 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5C27D00827C7D8B500DD6182 /* libgmpxx.a in Frameworks */, - 5C27D01027C7D8B500DD6182 /* libHSsimplex-chat-1.2.1-KSWVFEZPyZUBOUtDs8BKKY-ghc8.10.7.a in Frameworks */, - 5C27D00A27C7D8B500DD6182 /* libgmp.a in Frameworks */, 5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */, 5C764E83279C748B000C6508 /* libz.tbd in Frameworks */, - 5C27D00E27C7D8B500DD6182 /* libffi.a in Frameworks */, - 5C27D00C27C7D8B500DD6182 /* libHSsimplex-chat-1.2.1-KSWVFEZPyZUBOUtDs8BKKY.a in Frameworks */, + 5C67D32027D0003A00E4261F /* libgmp.a in Frameworks */, + 5C67D31C27D0003A00E4261F /* libHSsimplex-chat-1.3.0-5IozqhzNoFs59GB71w8Qla.a in Frameworks */, 5C764E82279C748B000C6508 /* libiconv.tbd in Frameworks */, + 5C67D31A27D0003A00E4261F /* libffi.a in Frameworks */, + 5C67D31827D0003A00E4261F /* libHSsimplex-chat-1.3.0-5IozqhzNoFs59GB71w8Qla-ghc8.10.7.a in Frameworks */, + 5C67D31E27D0003A00E4261F /* libgmpxx.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -201,13 +201,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5C27D00927C7D8B500DD6182 /* libgmpxx.a in Frameworks */, - 5C27D00F27C7D8B500DD6182 /* libffi.a in Frameworks */, + 5C67D31F27D0003A00E4261F /* libgmpxx.a in Frameworks */, + 5C67D32127D0003A00E4261F /* libgmp.a in Frameworks */, + 5C67D31927D0003A00E4261F /* libHSsimplex-chat-1.3.0-5IozqhzNoFs59GB71w8Qla-ghc8.10.7.a in Frameworks */, 5C764E85279C748C000C6508 /* libz.tbd in Frameworks */, - 5C27D00B27C7D8B500DD6182 /* libgmp.a in Frameworks */, - 5C27D01127C7D8B500DD6182 /* libHSsimplex-chat-1.2.1-KSWVFEZPyZUBOUtDs8BKKY-ghc8.10.7.a in Frameworks */, - 5C27D00D27C7D8B500DD6182 /* libHSsimplex-chat-1.2.1-KSWVFEZPyZUBOUtDs8BKKY.a in Frameworks */, + 5C67D31D27D0003A00E4261F /* libHSsimplex-chat-1.3.0-5IozqhzNoFs59GB71w8Qla.a in Frameworks */, 5C764E84279C748C000C6508 /* libiconv.tbd in Frameworks */, + 5C67D31B27D0003A00E4261F /* libffi.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -259,11 +259,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5C27D00627C7D8B500DD6182 /* libffi.a */, - 5C27D00427C7D8B500DD6182 /* libgmp.a */, - 5C27D00327C7D8B500DD6182 /* libgmpxx.a */, - 5C27D00727C7D8B500DD6182 /* libHSsimplex-chat-1.2.1-KSWVFEZPyZUBOUtDs8BKKY-ghc8.10.7.a */, - 5C27D00527C7D8B500DD6182 /* libHSsimplex-chat-1.2.1-KSWVFEZPyZUBOUtDs8BKKY.a */, + 5C67D31427D0003A00E4261F /* libffi.a */, + 5C67D31727D0003A00E4261F /* libgmp.a */, + 5C67D31627D0003A00E4261F /* libgmpxx.a */, + 5C67D31327D0003A00E4261F /* libHSsimplex-chat-1.3.0-5IozqhzNoFs59GB71w8Qla-ghc8.10.7.a */, + 5C67D31527D0003A00E4261F /* libHSsimplex-chat-1.3.0-5IozqhzNoFs59GB71w8Qla.a */, ); path = Libraries; sourceTree = ""; @@ -815,7 +815,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 21; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -835,7 +835,7 @@ ); "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/Libraries/ios"; "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/Libraries/sim"; - MARKETING_VERSION = 0.4; + MARKETING_VERSION = 1.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -855,7 +855,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 21; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -875,7 +875,7 @@ ); "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/Libraries/ios"; "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/Libraries/sim"; - MARKETING_VERSION = 0.4; + MARKETING_VERSION = 1.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; diff --git a/cabal.project b/cabal.project index 0ae6c0efd7..609b8ef40b 100644 --- a/cabal.project +++ b/cabal.project @@ -3,7 +3,7 @@ packages: . source-repository-package type: git location: git://github.com/simplex-chat/simplexmq.git - tag: d1e6147adfbd46f5e3e996cc6365d8f3f0f7669c + tag: 7a19ab224bdd1122f0761704b6ca1eb4e1e26eb7 source-repository-package type: git diff --git a/entropy.patch b/entropy.patch new file mode 100644 index 0000000000..2add42acb3 --- /dev/null +++ b/entropy.patch @@ -0,0 +1,30 @@ +diff --git a/cbits/random_initialized.c b/cbits/random_initialized.c +index 36ac968..ab708b0 100644 +--- a/cbits/random_initialized.c ++++ b/cbits/random_initialized.c +@@ -5,14 +5,6 @@ + #include + #include + +-#ifdef HAVE_GETENTROPY +-static int ensure_pool_initialized_getentropy() +-{ +- char tmp; +- return getentropy(&tmp, sizeof(tmp)); +-} +-#endif +- + // Poll /dev/random to wait for randomness. This is a proxy for the /dev/urandom + // pool being initialized. + static int ensure_pool_initialized_poll() +@@ -45,10 +37,5 @@ static int ensure_pool_initialized_poll() + // Returns 0 on success, non-zero on failure. + int ensure_pool_initialized() + { +-#ifdef HAVE_GETENTROPY +- if (ensure_pool_initialized_getentropy() == 0) +- return 0; +-#endif +- + return ensure_pool_initialized_poll(); + } diff --git a/flake.lock b/flake.lock index 8ab1093a0c..dc0bdac4e7 100644 --- a/flake.lock +++ b/flake.lock @@ -20,10 +20,10 @@ "flake": false, "locked": { "lastModified": 1603716527, - "narHash": "sha256-sDbrmur9Zfp4mPKohCD8IDZfXJ0Tjxpmr2R+kg5PpSY=", + "narHash": "sha256-X0TFfdD4KZpwl0Zr6x+PLxUt/VyKQfX7ylXHdmZIL+w=", "owner": "haskell", "repo": "cabal", - "rev": "94aaa8e4720081f9c75497e2735b90f6a819b08e", + "rev": "48bf10787e27364730dd37a42b603cee8d6af7ee", "type": "github" }, "original": { @@ -36,11 +36,11 @@ "cabal-34": { "flake": false, "locked": { - "lastModified": 1622475795, - "narHash": "sha256-chwTL304Cav+7p38d9mcb+egABWmxo2Aq+xgVBgEb/U=", + "lastModified": 1640353650, + "narHash": "sha256-N1t6M3/wqj90AEdRkeC8i923gQYUpzSr8b40qVOZ1Rk=", "owner": "haskell", "repo": "cabal", - "rev": "b086c1995cdd616fc8d91f46a21e905cc50a1049", + "rev": "942639c18c0cd8ec53e0a6f8d120091af35312cd", "type": "github" }, "original": { @@ -53,11 +53,11 @@ "cabal-36": { "flake": false, "locked": { - "lastModified": 1640163203, - "narHash": "sha256-TwDWP2CffT0j40W6zr0J1Qbu+oh3nsF1lUx9446qxZM=", + "lastModified": 1641652457, + "narHash": "sha256-BlFPKP4C4HRUJeAbdembX1Rms1LD380q9s0qVDeoAak=", "owner": "haskell", "repo": "cabal", - "rev": "ecf418050c1821f25e2e218f1be94c31e0465df1", + "rev": "f27667f8ec360c475027dcaee0138c937477b070", "type": "github" }, "original": { @@ -100,11 +100,11 @@ }, "flake-utils_2": { "locked": { - "lastModified": 1623875721, - "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=", + "lastModified": 1644229661, + "narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=", "owner": "numtide", "repo": "flake-utils", - "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772", + "rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797", "type": "github" }, "original": { @@ -133,11 +133,11 @@ "hackage": { "flake": false, "locked": { - "lastModified": 1642986764, - "narHash": "sha256-U6FPiNjz9JctwKC838LEoT/xjGfb8L18ZGIEY5YYzdU=", + "lastModified": 1646097829, + "narHash": "sha256-PcHDDV8NuUxZhPV/p++IkZC+SDZ1Db7m7K+9HN4/0S4=", "owner": "input-output-hk", "repo": "hackage.nix", - "rev": "22406c79a506164c4e835a68e54739f63f918784", + "rev": "283f096976b48e54183905e7bdde7f213c6ee5cd", "type": "github" }, "original": { @@ -169,16 +169,16 @@ "stackage": "stackage" }, "locked": { - "lastModified": 1643019329, - "narHash": "sha256-So77czYvvD0jt4GJeypkqw3VNn20ype5tHnHri2s5lg=", + "lastModified": 1646134763, + "narHash": "sha256-/p+N9TB57Eq0lrJ7gTH2YLxHo/mZ8sT2g9oKMsAh+0M=", "owner": "input-output-hk", "repo": "haskell.nix", - "rev": "ddc654e2e7e44617bfc17a5aed2a0947d3e192cc", + "rev": "d5f81c2e4cd9166a5f342b3469813b56455be173", "type": "github" }, "original": { "owner": "input-output-hk", - "ref": "angerman/android-static", + "ref": "angerman/try-no-libcharset", "repo": "haskell.nix", "type": "github" } @@ -202,11 +202,11 @@ "nix-tools": { "flake": false, "locked": { - "lastModified": 1636018067, - "narHash": "sha256-ng306fkuwr6V/malWtt3979iAC4yMVDDH2ViwYB6sQE=", + "lastModified": 1644395812, + "narHash": "sha256-BVFk/BEsTLq5MMZvdy3ZYHKfaS3dHrsKh4+tb5t5b58=", "owner": "input-output-hk", "repo": "nix-tools", - "rev": "ed5bd7215292deba55d6ab7a4e8c21f8b1564dda", + "rev": "d847c63b99bbec78bf83be2a61dc9f09b8a9ccc1", "type": "github" }, "original": { @@ -217,16 +217,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1641457028, - "narHash": "sha256-bA31xSpdSIo+rJMbHPurlxIsP/b6bbN+jvXOqyn2lR8=", - "owner": "angerman", + "lastModified": 1645623357, + "narHash": "sha256-vAaI91QFn/kY/uMiebW+kG2mPmxirMSJWYtkqkBKdDc=", + "owner": "nixos", "repo": "nixpkgs", - "rev": "7b049e87e9b371f9ea6648aa8f1f2d17b2e31ae5", + "rev": "9222ae36b208d1c6b55d88e10aa68f969b5b5244", "type": "github" }, "original": { - "owner": "angerman", - "ref": "patch-1", + "owner": "nixos", + "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } @@ -249,11 +249,11 @@ }, "nixpkgs-2105": { "locked": { - "lastModified": 1640283157, - "narHash": "sha256-6Ddfop+rKE+Gl9Tjp9YIrkfoYPzb8F80ergdjcq3/MY=", + "lastModified": 1642244250, + "narHash": "sha256-vWpUEqQdVP4srj+/YLJRTN9vjpTs4je0cdWKXPbDItc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "dde1557825c5644c869c5efc7448dc03722a8f09", + "rev": "0fd9ee1aa36ce865ad273f4f07fdc093adeb5c00", "type": "github" }, "original": { @@ -265,11 +265,11 @@ }, "nixpkgs-2111": { "locked": { - "lastModified": 1640283207, - "narHash": "sha256-SCwl7ZnCfMDsuSYvwIroiAlk7n33bW8HFfY8NvKhcPA=", + "lastModified": 1644510859, + "narHash": "sha256-xjpVvL5ecbyi0vxtVl/Fh9bwGlMbw3S06zE5nUzFB8A=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "64c7e3388bbd9206e437713351e814366e0c3284", + "rev": "0d1d5d7e3679fec9d07f2eb804d9f9fdb98378d3", "type": "github" }, "original": { @@ -281,11 +281,11 @@ }, "nixpkgs-unstable": { "locked": { - "lastModified": 1641285291, - "narHash": "sha256-KYaOBNGar3XWTxTsYPr9P6u74KAqNq0wobEC236U+0c=", + "lastModified": 1644486793, + "narHash": "sha256-EeijR4guVHgVv+JpOX3cQO+1XdrkJfGmiJ9XVsVU530=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "0432195a4b8d68faaa7d3d4b355260a3120aeeae", + "rev": "1882c6b7368fd284ad01b0a5b5601ef136321292", "type": "github" }, "original": { @@ -322,11 +322,11 @@ "stackage": { "flake": false, "locked": { - "lastModified": 1642986888, - "narHash": "sha256-oxG7LzlJdjKTJgSv7diKWsGTETDZMPT2mNNLbrBfiVs=", + "lastModified": 1646010978, + "narHash": "sha256-NpioQiTXyYm+Gm111kcDEE/ghflmnTNwPhWff54GYA4=", "owner": "input-output-hk", "repo": "stackage.nix", - "rev": "aeaf5fe21874f01702f394d01e18f472be6e3e08", + "rev": "9cce3e0d420f6c38cdbbe1c5e5bbc07fd2adfc3a", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index a827eabc56..ecd71e5f95 100644 --- a/flake.nix +++ b/flake.nix @@ -1,7 +1,7 @@ { description = "nix flake for simplex-chat"; - inputs.nixpkgs.url = "github:angerman/nixpkgs/patch-1"; # based on 21.11, still need this, until everything is merged into 21.11. - inputs.haskellNix.url = "github:input-output-hk/haskell.nix?ref=angerman/android-static"; + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; #angerman/nixpkgs/patch-1"; # based on 21.11, still need this, until everything is merged into 21.11. + inputs.haskellNix.url = "github:input-output-hk/haskell.nix?ref=angerman/try-no-libcharset"; inputs.haskellNix.inputs.nixpkgs.follows = "nixpkgs"; inputs.flake-utils.url = "github:numtide/flake-utils"; outputs = { self, haskellNix, nixpkgs, flake-utils }: @@ -21,6 +21,7 @@ sha256map = import ./sha256map.nix; modules = [{ packages.direct-sqlite.patches = [ ./direct-sqlite-2.3.26.patch ]; + packages.entropy.patches = [ ./entropy.patch ]; } ({ pkgs,lib, ... }: lib.mkIf (pkgs.stdenv.hostPlatform.isAndroid) { packages.simplex-chat.components.library.ghcOptions = [ "-pie" ]; @@ -45,76 +46,170 @@ "exe:simplex-chat" = (drv pkgs).simplex-chat.components.exes.simplex-chat; } // ({ "x86_64-linux" = - let - androidPkgs = pkgs.pkgsCross.aarch64-android; - # For some reason building libiconv with nixpgks android setup produces - # LANGINFO_CODESET to be found, which is not compatible with android sdk 23; - # so we'll patch up iconv to not include that. - androidIconv = (androidPkgs.libiconv.override { enableStatic = true; }).overrideAttrs (old: { - postConfigure = '' - echo "#undef HAVE_LANGINFO_CODESET" >> libcharset/config.h - echo "#undef HAVE_LANGINFO_CODESET" >> lib/config.h - ''; - }); - # Similarly to icovn, for reasons beyond my current knowledge, nixpkgs andorid - # toolchain makes configure believe we have MEMFD_CREATE, which we don't in - # sdk 23. - androidFFI = androidPkgs.libffi.overrideAttrs (old: { - dontDisableStatic = true; - hardeningDisable = [ "fortify" ]; - postConfigure = '' - echo "#undef HAVE_MEMFD_CREATE" >> aarch64-unknown-linux-android/fficonfig.h - ''; - } - );in { - "aarch64-android:lib:support" = (drv androidPkgs).android-support.components.library.override { - smallAddressSpace = true; enableShared = false; - setupBuildFlags = map (x: "--ghc-option=${x}") [ "-shared" "-o" "libsupport.so" ]; - postInstall = '' - - mkdir -p $out/_pkg - cp libsupport.so $out/_pkg - ${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so.1 $out/_pkg/libsupport.so - (cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/pkg-android-libsupport.zip *) - rm -fR $out/_pkg - - mkdir -p $out/nix-support - echo "file binary-dist \"$(echo $out/*.zip)\"" \ - > $out/nix-support/hydra-build-products - ''; - }; - "aarch64-android:lib:simplex-chat" = (drv androidPkgs).simplex-chat.components.library.override { - smallAddressSpace = true; enableShared = false; - # for android we build a shared library, passing these arguments is a bit tricky, as - # we want only the threaded rts (HSrts_thr) and ffi to be linked, but not fed into iserv for - # template haskell cross compilation. Thus we just pass them as linker options (-optl). - setupBuildFlags = map (x: "--ghc-option=${x}") [ "-shared" "-o" "libsimplex.so" "-optl-lHSrts_thr" "-optl-lffi"]; - postInstall = '' - ${pkgs.tree}/bin/tree $out - mkdir -p $out/_pkg - # copy over includes, we might want those, but maybe not. - # cp -r $out/lib/*/*/include $out/_pkg/ - # find the libHS...ghc-X.Y.Z.a static library; this is the - # rolled up one with all dependencies included. - cp libsimplex.so $out/_pkg - # find ./dist -name "lib*.so" -exec cp {} $out/_pkg \; - # find ./dist -name "libHS*-ghc*.a" -exec cp {} $out/_pkg \; - # find ${androidFFI}/lib -name "*.a" -exec cp {} $out/_pkg \; - # find ${androidPkgs.gmp6.override { withStatic = true; }}/lib -name "*.a" -exec cp {} $out/_pkg \; - # find ${androidIconv}/lib -name "*.a" -exec cp {} $out/_pkg \; - # find ${androidPkgs.stdenv.cc.libc}/lib -name "*.a" -exec cp {} $out/_pkg \; - - ${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so.1 $out/_pkg/libsimplex.so - - ${pkgs.tree}/bin/tree $out/_pkg - (cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/pkg-android-libsimplex.zip *) - rm -fR $out/_pkg - mkdir -p $out/nix-support - echo "file binary-dist \"$(echo $out/*.zip)\"" \ - > $out/nix-support/hydra-build-products + let + androidPkgs = pkgs.pkgsCross.aarch64-android; + # For some reason building libiconv with nixpgks android setup produces + # LANGINFO_CODESET to be found, which is not compatible with android sdk 23; + # so we'll patch up iconv to not include that. + androidIconv = (androidPkgs.libiconv.override { enableStatic = true; }).overrideAttrs (old: { + postConfigure = '' + echo "#undef HAVE_LANGINFO_CODESET" >> libcharset/config.h + echo "#undef HAVE_LANGINFO_CODESET" >> lib/config.h ''; - }; - }; + }); + # Similarly to icovn, for reasons beyond my current knowledge, nixpkgs andorid + # toolchain makes configure believe we have MEMFD_CREATE, which we don't in + # sdk 23. + androidFFI = androidPkgs.libffi.overrideAttrs (old: { + dontDisableStatic = true; + hardeningDisable = [ "fortify" ]; + postConfigure = '' + echo "#undef HAVE_MEMFD_CREATE" >> aarch64-unknown-linux-android/fficonfig.h + ''; + } + );in { + "aarch64-android:lib:support" = (drv androidPkgs).android-support.components.library.override { + smallAddressSpace = true; enableShared = false; + setupBuildFlags = map (x: "--ghc-option=${x}") [ "-shared" "-o" "libsupport.so" ]; + postInstall = '' + + mkdir -p $out/_pkg + cp libsupport.so $out/_pkg + ${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so.1 $out/_pkg/libsupport.so + (cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/pkg-aarch64-android-libsupport.zip *) + rm -fR $out/_pkg + + mkdir -p $out/nix-support + echo "file binary-dist \"$(echo $out/*.zip)\"" \ + > $out/nix-support/hydra-build-products + ''; + }; + "aarch64-android:lib:simplex-chat" = (drv androidPkgs).simplex-chat.components.library.override { + smallAddressSpace = true; enableShared = false; + # for android we build a shared library, passing these arguments is a bit tricky, as + # we want only the threaded rts (HSrts_thr) and ffi to be linked, but not fed into iserv for + # template haskell cross compilation. Thus we just pass them as linker options (-optl). + setupBuildFlags = map (x: "--ghc-option=${x}") [ "-shared" "-o" "libsimplex.so" "-optl-lHSrts_thr" "-optl-lffi"]; + postInstall = '' + ${pkgs.tree}/bin/tree $out + mkdir -p $out/_pkg + # copy over includes, we might want those, but maybe not. + # cp -r $out/lib/*/*/include $out/_pkg/ + # find the libHS...ghc-X.Y.Z.a static library; this is the + # rolled up one with all dependencies included. + cp libsimplex.so $out/_pkg + # find ./dist -name "lib*.so" -exec cp {} $out/_pkg \; + # find ./dist -name "libHS*-ghc*.a" -exec cp {} $out/_pkg \; + # find ${androidFFI}/lib -name "*.a" -exec cp {} $out/_pkg \; + # find ${androidPkgs.gmp6.override { withStatic = true; }}/lib -name "*.a" -exec cp {} $out/_pkg \; + # find ${androidIconv}/lib -name "*.a" -exec cp {} $out/_pkg \; + # find ${androidPkgs.stdenv.cc.libc}/lib -name "*.a" -exec cp {} $out/_pkg \; + + ${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so.1 $out/_pkg/libsimplex.so + + ${pkgs.tree}/bin/tree $out/_pkg + (cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/pkg-aarch64-android-libsimplex.zip *) + rm -fR $out/_pkg + mkdir -p $out/nix-support + echo "file binary-dist \"$(echo $out/*.zip)\"" \ + > $out/nix-support/hydra-build-products + ''; + }; + "x86_64-android:lib:support" = (drv androidPkgs).android-support.components.library.override { + smallAddressSpace = true; enableShared = false; + setupBuildFlags = map (x: "--ghc-option=${x}") [ "-shared" "-o" "libsupport.so" ]; + postInstall = '' + + mkdir -p $out/_pkg + cp libsupport.so $out/_pkg + ${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so.1 $out/_pkg/libsupport.so + (cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/pkg-x86_64-android-libsupport.zip *) + rm -fR $out/_pkg + + mkdir -p $out/nix-support + echo "file binary-dist \"$(echo $out/*.zip)\"" \ + > $out/nix-support/hydra-build-products + ''; + }; + "x86_64-android:lib:simplex-chat" = (drv androidPkgs).simplex-chat.components.library.override { + smallAddressSpace = true; enableShared = false; + # for android we build a shared library, passing these arguments is a bit tricky, as + # we want only the threaded rts (HSrts_thr) and ffi to be linked, but not fed into iserv for + # template haskell cross compilation. Thus we just pass them as linker options (-optl). + setupBuildFlags = map (x: "--ghc-option=${x}") [ "-shared" "-o" "libsimplex.so" "-optl-lHSrts_thr" "-optl-lffi"]; + postInstall = '' + ${pkgs.tree}/bin/tree $out + mkdir -p $out/_pkg + # copy over includes, we might want those, but maybe not. + # cp -r $out/lib/*/*/include $out/_pkg/ + # find the libHS...ghc-X.Y.Z.a static library; this is the + # rolled up one with all dependencies included. + cp libsimplex.so $out/_pkg + # find ./dist -name "lib*.so" -exec cp {} $out/_pkg \; + # find ./dist -name "libHS*-ghc*.a" -exec cp {} $out/_pkg \; + # find ${androidFFI}/lib -name "*.a" -exec cp {} $out/_pkg \; + # find ${androidPkgs.gmp6.override { withStatic = true; }}/lib -name "*.a" -exec cp {} $out/_pkg \; + # find ${androidIconv}/lib -name "*.a" -exec cp {} $out/_pkg \; + # find ${androidPkgs.stdenv.cc.libc}/lib -name "*.a" -exec cp {} $out/_pkg \; + + ${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so.1 $out/_pkg/libsimplex.so + + ${pkgs.tree}/bin/tree $out/_pkg + (cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/pkg-x86_64-android-libsimplex.zip *) + rm -fR $out/_pkg + mkdir -p $out/nix-support + echo "file binary-dist \"$(echo $out/*.zip)\"" \ + > $out/nix-support/hydra-build-products + ''; + }; + "x86_64-linux:lib:support" = (drv androidPkgs).android-support.components.library.override { + smallAddressSpace = true; enableShared = false; + setupBuildFlags = map (x: "--ghc-option=${x}") [ "-shared" "-o" "libsupport.so" ]; + postInstall = '' + + mkdir -p $out/_pkg + cp libsupport.so $out/_pkg + ${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so.1 $out/_pkg/libsupport.so + (cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/pkg-x86_64-linux-libsupport.zip *) + rm -fR $out/_pkg + + mkdir -p $out/nix-support + echo "file binary-dist \"$(echo $out/*.zip)\"" \ + > $out/nix-support/hydra-build-products + ''; + }; + "x86_64-linux:lib:simplex-chat" = (drv androidPkgs).simplex-chat.components.library.override { + smallAddressSpace = true; enableShared = false; + # for android we build a shared library, passing these arguments is a bit tricky, as + # we want only the threaded rts (HSrts_thr) and ffi to be linked, but not fed into iserv for + # template haskell cross compilation. Thus we just pass them as linker options (-optl). + setupBuildFlags = map (x: "--ghc-option=${x}") [ "-shared" "-o" "libsimplex.so" "-optl-lHSrts_thr" "-optl-lffi"]; + postInstall = '' + ${pkgs.tree}/bin/tree $out + mkdir -p $out/_pkg + # copy over includes, we might want those, but maybe not. + # cp -r $out/lib/*/*/include $out/_pkg/ + # find the libHS...ghc-X.Y.Z.a static library; this is the + # rolled up one with all dependencies included. + cp libsimplex.so $out/_pkg + # find ./dist -name "lib*.so" -exec cp {} $out/_pkg \; + # find ./dist -name "libHS*-ghc*.a" -exec cp {} $out/_pkg \; + # find ${androidFFI}/lib -name "*.a" -exec cp {} $out/_pkg \; + # find ${androidPkgs.gmp6.override { withStatic = true; }}/lib -name "*.a" -exec cp {} $out/_pkg \; + # find ${androidIconv}/lib -name "*.a" -exec cp {} $out/_pkg \; + # find ${androidPkgs.stdenv.cc.libc}/lib -name "*.a" -exec cp {} $out/_pkg \; + + ${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so.1 $out/_pkg/libsimplex.so + + ${pkgs.tree}/bin/tree $out/_pkg + (cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/pkg-x86_64-linux-libsimplex.zip *) + rm -fR $out/_pkg + mkdir -p $out/nix-support + echo "file binary-dist \"$(echo $out/*.zip)\"" \ + > $out/nix-support/hydra-build-products + ''; + }; + }; "aarch64-darwin" = { "aarch64-darwin:lib:simplex-chat" = (drv pkgs).simplex-chat.components.library.override { smallAddressSpace = true; enableShared = false; diff --git a/package.yaml b/package.yaml index 111b557f0d..1d1ee367ec 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 1.3.0 +version: 1.3.1 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/sha256map.nix b/sha256map.nix index 8b428b97eb..6f6c57d8b4 100644 --- a/sha256map.nix +++ b/sha256map.nix @@ -1,5 +1,5 @@ { - "git://github.com/simplex-chat/simplexmq.git"."d1e6147adfbd46f5e3e996cc6365d8f3f0f7669c" = "11wny0ivhrrp36757i074ml18k6nv7hq6a5dvv4rg3npqf19y3r7"; + "git://github.com/simplex-chat/simplexmq.git"."7a19ab224bdd1122f0761704b6ca1eb4e1e26eb7" = "1sn2bzz5v2r6wxf1p2k9578zwp0vlb42lb6xjqwpl4acr47wcx0g"; "git://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "git://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "git://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 053d771f31..ee2cf0b4b4 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 1.3.0 +version: 1.3.1 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index d12a5cfa6e..d5afd8003a 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -144,7 +144,10 @@ processChatCommand = \case user <- withStore $ \st -> createUser st p True atomically . writeTVar u $ Just user pure $ CRActiveUser user - StartChat -> withUser' $ \user -> startChatController user $> CRChatStarted + StartChat -> withUser' $ \user -> + asks agentAsync >>= readTVarIO >>= \case + Just _ -> pure CRChatRunning + _ -> startChatController user $> CRChatStarted APIGetChats -> CRApiChats <$> withUser (\user -> withStore (`getChatPreviews` user)) APIGetChat cType cId pagination -> withUser $ \user -> case cType of CTDirect -> CRApiChat . AChat SCTDirect <$> withStore (\st -> getDirectChat st user cId pagination) @@ -528,7 +531,9 @@ subscribeUserConnections user@User {userId} = do subscribe cId `catchError` (toView . CRRcvFileSubError ft) subscribePendingConnections n = do cs <- withStore (`getPendingConnections` user) - subscribeConns n cs `catchError` \_ -> pure () + summary <- pooledForConcurrentlyN n cs $ \Connection {agentConnId = acId@(AgentConnId cId)} -> + PendingSubStatus acId <$> ((subscribe cId $> Nothing) `catchError` (pure . Just)) + toView $ CRPendingSubSummary summary subscribeUserContactLink n = do cs <- withStore (`getUserContactLinkConnections` userId) (subscribeConns n cs >> toView CRUserContactLinkSubscribed) diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 25c7a10270..6b44a6e477 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -36,7 +36,7 @@ import System.IO (Handle) import UnliftIO.STM versionNumber :: String -versionNumber = "1.3.0" +versionNumber = "1.3.1" versionStr :: String versionStr = "SimpleX Chat v" <> versionNumber @@ -133,6 +133,7 @@ data ChatCommand data ChatResponse = CRActiveUser {user :: User} | CRChatStarted + | CRChatRunning | CRApiChats {chats :: [AChat]} | CRApiChat {chat :: AChat} | CRNewChatItem {chatItem :: AChatItem} @@ -204,6 +205,7 @@ data ChatResponse | CRMemberSubError {groupInfo :: GroupInfo, contactName :: ContactName, chatError :: ChatError} -- TODO Contact? or GroupMember? | CRMemberSubErrors {memberSubErrors :: [MemberSubError]} | CRGroupSubscribed {groupInfo :: GroupInfo} + | CRPendingSubSummary {pendingSubStatus :: [PendingSubStatus]} | CRSndFileSubError {sndFileTransfer :: SndFileTransfer, chatError :: ChatError} | CRRcvFileSubError {rcvFileTransfer :: RcvFileTransfer, chatError :: ChatError} | CRUserContactLinkSubscribed @@ -236,6 +238,16 @@ data MemberSubError = MemberSubError instance ToJSON MemberSubError where toEncoding = J.genericToEncoding J.defaultOptions +data PendingSubStatus = PendingSubStatus + { connId :: AgentConnId, + connError :: Maybe ChatError + } + deriving (Show, Generic) + +instance ToJSON PendingSubStatus where + toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} + toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} + data ChatError = ChatError {errorType :: ChatErrorType} | ChatErrorAgent {agentError :: AgentErrorType} diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 8b829241ab..be007c19e3 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -20,6 +20,7 @@ import Simplex.Chat.Controller import Simplex.Chat.Options import Simplex.Chat.Store import Simplex.Chat.Types +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (yesToMigrations)) import Simplex.Messaging.Protocol (CorrId (..)) foreign export ccall "chat_init" cChatInit :: CString -> IO (StablePtr ChatController) @@ -57,7 +58,7 @@ defaultMobileConfig :: ChatConfig defaultMobileConfig = defaultChatConfig { yesToMigrations = True, - agentConfig = agentConfig defaultChatConfig {yesToMigrations = True} + agentConfig = (agentConfig defaultChatConfig) {yesToMigrations = True} } type CJSONString = CString @@ -68,7 +69,7 @@ getActiveUser_ st = find activeUser <$> getUsers st chatInit :: String -> IO ChatController chatInit dbFilePrefix = do let f = chatStoreFile dbFilePrefix - chatStore <- createStore f (dbPoolSize defaultMobileConfig) (yesToMigrations defaultMobileConfig) + chatStore <- createStore f (dbPoolSize defaultMobileConfig) (yesToMigrations (defaultMobileConfig :: ChatConfig)) user_ <- getActiveUser_ chatStore newChatController chatStore user_ defaultMobileConfig mobileChatOpts {dbFilePrefix} (const $ pure ()) diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs index 1d4f570b8f..06b90edaa0 100644 --- a/src/Simplex/Chat/Options.hs +++ b/src/Simplex/Chat/Options.hs @@ -28,10 +28,9 @@ data ChatOpts = ChatOpts defaultSMPServers :: NonEmpty SMPServer defaultSMPServers = L.fromList - [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im", - "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im", - "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im" - -- "smp://Tn1b3Rr7_gErbVt2v50Y_T-PvUAi1BYAMS-62w-k9CI=@139.162.240.237" + [ "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im", + "smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im", + "smp://6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE=@smp10.simplex.im" ] chatOpts :: FilePath -> Parser ChatOpts @@ -51,8 +50,7 @@ chatOpts appDir = <> short 's' <> metavar "SERVER" <> help - "Comma separated list of SMP server(s) to use \ - \(default: smp4.simplex.im,smp5.simplex.im,smp6.simplex.im)" + "Comma separated list of SMP server(s) to use" <> value defaultSMPServers ) <*> switch diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index c1d35e3079..ace8eeb06c 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -15,10 +15,12 @@ module Simplex.Chat.Protocol where import Control.Monad ((<=<)) import Data.Aeson (FromJSON, ToJSON, (.:), (.:?), (.=)) import qualified Data.Aeson as J +import qualified Data.Aeson.Encoding as JE import qualified Data.Aeson.KeyMap as JM import qualified Data.Aeson.Types as JT import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteString.Lazy.Char8 as LB +import Data.Maybe (fromMaybe) import Data.Text (Text) import Data.Text.Encoding (decodeLatin1, encodeUtf8) import Database.SQLite.Simple.FromField (FromField (..)) @@ -107,26 +109,24 @@ instance ToJSON MsgContentType where toJSON = strToJSON toEncoding = strToJEncoding --- TODO - include tag and original JSON into MCUnknown so that information is not lost --- so when it serializes back it is the same as it was and chat upgrade makes it readable -data MsgContent = MCText Text | MCUnknown +data MsgContent = MCText Text | MCUnknown J.Value Text deriving (Eq, Show) msgContentText :: MsgContent -> Text msgContentText = \case MCText t -> t - MCUnknown -> unknownMsgType + MCUnknown _ t -> t toMsgContentType :: MsgContent -> MsgContentType toMsgContentType = \case MCText _ -> MCText_ - MCUnknown -> MCUnknown_ + MCUnknown {} -> MCUnknown_ instance FromJSON MsgContent where - parseJSON (J.Object v) = do + parseJSON jv@(J.Object v) = do v .: "type" >>= \case MCText_ -> MCText <$> v .: "text" - MCUnknown_ -> pure MCUnknown + MCUnknown_ -> MCUnknown jv . fromMaybe unknownMsgType <$> v .:? "text" parseJSON invalid = JT.prependFailure "bad MsgContent, " (JT.typeMismatch "Object" invalid) @@ -134,16 +134,12 @@ unknownMsgType :: Text unknownMsgType = "unknown message type" instance ToJSON MsgContent where - toJSON mc = - J.object $ - ("type" .= toMsgContentType mc) : case mc of - MCText t -> ["text" .= t] - MCUnknown -> ["text" .= unknownMsgType] - toEncoding mc = - J.pairs $ - ("type" .= toMsgContentType mc) <> case mc of - MCText t -> "text" .= t - MCUnknown -> "text" .= unknownMsgType + toJSON = \case + MCUnknown v _ -> v + MCText t -> J.object ["type" .= MCText_, "text" .= t] + toEncoding = \case + MCUnknown v _ -> JE.value v + MCText t -> J.pairs $ "type" .= MCText_ <> "text" .= t data CMEventTag = XMsgNew_ diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index f0286d7251..ecfb033a6e 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -39,6 +39,7 @@ responseToView :: Bool -> ChatResponse -> [StyledString] responseToView testView = \case CRActiveUser User {profile} -> viewUserProfile profile CRChatStarted -> ["chat started"] + CRChatRunning -> [] CRApiChats chats -> if testView then testViewChats chats else [plain . bshow $ J.encode chats] CRApiChat chat -> if testView then testViewChat chat else [plain . bshow $ J.encode chat] CRNewChatItem (AChatItem _ _ chat item) -> viewChatItem chat item @@ -103,9 +104,9 @@ responseToView testView = \case CRContactSubscribed c -> [ttyContact' c <> ": connected to server"] CRContactSubError c e -> [ttyContact' c <> ": contact error " <> sShow e] CRContactSubSummary summary -> - (if null connected then [] else [sShow (length connected) <> " contacts connected (use " <> highlight' "/cs" <> " for the list)"]) <> viewErrorsSummary errors " contact errors" + (if null subscribed then [] else [sShow (length subscribed) <> " contacts connected (use " <> highlight' "/cs" <> " for the list)"]) <> viewErrorsSummary errors " contact errors" where - (errors, connected) = partition (isJust . contactError) summary + (errors, subscribed) = partition (isJust . contactError) summary CRGroupInvitation GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}} -> [groupInvitation ldn fullName] CRReceivedGroupInvitation g c role -> viewReceivedGroupInvitation g c role @@ -122,6 +123,7 @@ responseToView testView = \case CRMemberSubError g c e -> [ttyGroup' g <> " member " <> ttyContact c <> " error: " <> sShow e] CRMemberSubErrors summary -> viewErrorsSummary summary " group member errors" CRGroupSubscribed g -> [ttyFullGroup g <> ": connected to server(s)"] + CRPendingSubSummary _ -> [] CRSndFileSubError SndFileTransfer {fileId, fileName} e -> ["sent file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e] CRRcvFileSubError RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}} e -> @@ -369,7 +371,7 @@ ttyMsgTime = styleTime . formatTime defaultTimeLocale "%H:%M" ttyMsgContent :: MsgContent -> [StyledString] ttyMsgContent = \case MCText t -> msgPlain t - MCUnknown -> ["unknown message type"] + MCUnknown _ t -> msgPlain t ttySentFile :: StyledString -> FileTransferId -> FilePath -> [StyledString] ttySentFile to fId fPath = ["/f " <> to <> ttyFilePath fPath, "use " <> highlight ("/fc " <> show fId) <> " to cancel sending"] diff --git a/stack.yaml b/stack.yaml index 131d1766de..221d206e58 100644 --- a/stack.yaml +++ b/stack.yaml @@ -48,7 +48,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: d1e6147adfbd46f5e3e996cc6365d8f3f0f7669c + commit: 7a19ab224bdd1122f0761704b6ca1eb4e1e26eb7 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7