diff --git a/apps/ios/Shared/Views/NewChat/QRCode.swift b/apps/ios/Shared/Views/NewChat/QRCode.swift index 4bec3f8e6..82c4629c0 100644 --- a/apps/ios/Shared/Views/NewChat/QRCode.swift +++ b/apps/ios/Shared/Views/NewChat/QRCode.swift @@ -11,20 +11,12 @@ import CoreImage.CIFilterBuiltins struct MutableQRCode: View { @Binding var uri: String - @State private var image: UIImage? + var withLogo: Bool = true + var tintColor = UIColor(red: 0.023, green: 0.176, blue: 0.337, alpha: 1) var body: some View { - ZStack { - if let image = image { - qrCodeImage(image) - } - } - .onAppear { - image = generateImage(uri) - } - .onChange(of: uri) { _ in - image = generateImage(uri) - } + QRCode(uri: uri, withLogo: withLogo, tintColor: tintColor) + .id("simplex-qrcode-view-for-\(uri)") } } @@ -49,7 +41,7 @@ struct QRCode: View { var withLogo: Bool = true var tintColor = UIColor(red: 0.023, green: 0.176, blue: 0.337, alpha: 1) @State private var image: UIImage? = nil - @State private var makeScreenshotBinding: () -> Void = {} + @State private var makeScreenshotFunc: () -> Void = {} var body: some View { ZStack { @@ -70,18 +62,18 @@ struct QRCode: View { } } .onAppear { - makeScreenshotBinding = { + makeScreenshotFunc = { let size = CGSizeMake(1024 / UIScreen.main.scale, 1024 / UIScreen.main.scale) - showShareSheet(items: [makeScreenshot(geo.frame(in: .local).origin, size)])} + showShareSheet(items: [makeScreenshot(geo.frame(in: .local).origin, size)]) + } } .frame(width: geo.size.width, height: geo.size.height) } } - .onTapGesture(perform: makeScreenshotBinding) + .onTapGesture(perform: makeScreenshotFunc) .onAppear { - image = image ?? generateImage(uri)?.replaceColor(UIColor.black, tintColor) + image = image ?? generateImage(uri, tintColor: tintColor) } - } } @@ -93,13 +85,13 @@ private func qrCodeImage(_ image: UIImage) -> some View { .textSelection(.enabled) } -private func generateImage(_ uri: String) -> UIImage? { +private func generateImage(_ uri: String, tintColor: UIColor) -> UIImage? { let context = CIContext() let filter = CIFilter.qrCodeGenerator() filter.message = Data(uri.utf8) if let outputImage = filter.outputImage, let cgImage = context.createCGImage(outputImage, from: outputImage.extent) { - return UIImage(cgImage: cgImage) + return UIImage(cgImage: cgImage).replaceColor(UIColor.black, tintColor) } return nil } diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index 60e5cc7b0..e9657961e 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -190,7 +190,8 @@ struct UserAddressView: View { @ViewBuilder private func existingAddressView(_ userAddress: UserContactLink) -> some View { Section { - MutableQRCode(uri: Binding.constant(simplexChatLink(userAddress.connReqContact))) + SimpleXLinkQRCode(uri: userAddress.connReqContact) + .id("simplex-contact-address-qrcode-\(userAddress.connReqContact)") shareQRCodeButton(userAddress) if MFMailComposeViewController.canSendMail() { shareViaEmailButton(userAddress) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 8612370b0..1ddff81f9 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -43,6 +43,11 @@ 5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */; }; 5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */; }; 5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B3B09285FB130003915F2 /* DatabaseView.swift */; }; + 5C4BB4B82B1E7D75007981AA /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4BB4B32B1E7D75007981AA /* libgmp.a */; }; + 5C4BB4B92B1E7D75007981AA /* libHSsimplex-chat-5.4.0.6-DWi9o3X1dc6Jx2FZ4ew1fo-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4BB4B42B1E7D75007981AA /* libHSsimplex-chat-5.4.0.6-DWi9o3X1dc6Jx2FZ4ew1fo-ghc8.10.7.a */; }; + 5C4BB4BA2B1E7D75007981AA /* libHSsimplex-chat-5.4.0.6-DWi9o3X1dc6Jx2FZ4ew1fo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4BB4B52B1E7D75007981AA /* libHSsimplex-chat-5.4.0.6-DWi9o3X1dc6Jx2FZ4ew1fo.a */; }; + 5C4BB4BB2B1E7D75007981AA /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4BB4B62B1E7D75007981AA /* libffi.a */; }; + 5C4BB4BC2B1E7D75007981AA /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4BB4B72B1E7D75007981AA /* libgmpxx.a */; }; 5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; }; 5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A91E283AD0E400C4E99E /* CallManager.swift */; }; 5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */; }; @@ -120,11 +125,6 @@ 5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */; }; 5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */ = {isa = PBXBuildFile; fileRef = 5CD67B8E2B0E858A00C510B1 /* hs_init.c */; }; - 5CD67BA02B120ADF00C510B1 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CD67B9B2B120ADF00C510B1 /* libgmp.a */; }; - 5CD67BA12B120ADF00C510B1 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CD67B9C2B120ADF00C510B1 /* libgmpxx.a */; }; - 5CD67BA22B120ADF00C510B1 /* libHSsimplex-chat-5.4.0.6-9DfazyElTA72omjHp0C93u.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CD67B9D2B120ADF00C510B1 /* libHSsimplex-chat-5.4.0.6-9DfazyElTA72omjHp0C93u.a */; }; - 5CD67BA32B120ADF00C510B1 /* libHSsimplex-chat-5.4.0.6-9DfazyElTA72omjHp0C93u-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CD67B9E2B120ADF00C510B1 /* libHSsimplex-chat-5.4.0.6-9DfazyElTA72omjHp0C93u-ghc8.10.7.a */; }; - 5CD67BA42B120ADF00C510B1 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CD67B9F2B120ADF00C510B1 /* libffi.a */; }; 5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; }; 5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; }; 5CE2BA712845308900EC33A6 /* SimpleXChat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -295,6 +295,11 @@ 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettings.swift; sourceTree = ""; }; 5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = ""; }; 5C4B3B09285FB130003915F2 /* DatabaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseView.swift; sourceTree = ""; }; + 5C4BB4B32B1E7D75007981AA /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5C4BB4B42B1E7D75007981AA /* libHSsimplex-chat-5.4.0.6-DWi9o3X1dc6Jx2FZ4ew1fo-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.6-DWi9o3X1dc6Jx2FZ4ew1fo-ghc8.10.7.a"; sourceTree = ""; }; + 5C4BB4B52B1E7D75007981AA /* libHSsimplex-chat-5.4.0.6-DWi9o3X1dc6Jx2FZ4ew1fo.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.6-DWi9o3X1dc6Jx2FZ4ew1fo.a"; sourceTree = ""; }; + 5C4BB4B62B1E7D75007981AA /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 5C4BB4B72B1E7D75007981AA /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = ""; }; 5C55A91E283AD0E400C4E99E /* CallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = ""; }; 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = ""; }; @@ -408,11 +413,6 @@ 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanToConnectView.swift; sourceTree = ""; }; 5CD67B8D2B0E858A00C510B1 /* hs_init.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hs_init.h; sourceTree = ""; }; 5CD67B8E2B0E858A00C510B1 /* hs_init.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = hs_init.c; sourceTree = ""; }; - 5CD67B9B2B120ADF00C510B1 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5CD67B9C2B120ADF00C510B1 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 5CD67B9D2B120ADF00C510B1 /* libHSsimplex-chat-5.4.0.6-9DfazyElTA72omjHp0C93u.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.6-9DfazyElTA72omjHp0C93u.a"; sourceTree = ""; }; - 5CD67B9E2B120ADF00C510B1 /* libHSsimplex-chat-5.4.0.6-9DfazyElTA72omjHp0C93u-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.6-9DfazyElTA72omjHp0C93u-ghc8.10.7.a"; sourceTree = ""; }; - 5CD67B9F2B120ADF00C510B1 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; 5CDCAD452818589900503DA2 /* SimpleX NSE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX NSE.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 5CDCAD472818589900503DA2 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 5CDCAD492818589900503DA2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -521,13 +521,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5CD67BA12B120ADF00C510B1 /* libgmpxx.a in Frameworks */, - 5CD67BA22B120ADF00C510B1 /* libHSsimplex-chat-5.4.0.6-9DfazyElTA72omjHp0C93u.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5CD67BA02B120ADF00C510B1 /* libgmp.a in Frameworks */, - 5CD67BA42B120ADF00C510B1 /* libffi.a in Frameworks */, - 5CD67BA32B120ADF00C510B1 /* libHSsimplex-chat-5.4.0.6-9DfazyElTA72omjHp0C93u-ghc8.10.7.a in Frameworks */, + 5C4BB4BB2B1E7D75007981AA /* libffi.a in Frameworks */, + 5C4BB4BA2B1E7D75007981AA /* libHSsimplex-chat-5.4.0.6-DWi9o3X1dc6Jx2FZ4ew1fo.a in Frameworks */, + 5C4BB4B92B1E7D75007981AA /* libHSsimplex-chat-5.4.0.6-DWi9o3X1dc6Jx2FZ4ew1fo-ghc8.10.7.a in Frameworks */, + 5C4BB4B82B1E7D75007981AA /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, + 5C4BB4BC2B1E7D75007981AA /* libgmpxx.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -589,11 +589,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5CD67B9F2B120ADF00C510B1 /* libffi.a */, - 5CD67B9B2B120ADF00C510B1 /* libgmp.a */, - 5CD67B9C2B120ADF00C510B1 /* libgmpxx.a */, - 5CD67B9E2B120ADF00C510B1 /* libHSsimplex-chat-5.4.0.6-9DfazyElTA72omjHp0C93u-ghc8.10.7.a */, - 5CD67B9D2B120ADF00C510B1 /* libHSsimplex-chat-5.4.0.6-9DfazyElTA72omjHp0C93u.a */, + 5C4BB4B62B1E7D75007981AA /* libffi.a */, + 5C4BB4B32B1E7D75007981AA /* libgmp.a */, + 5C4BB4B72B1E7D75007981AA /* libgmpxx.a */, + 5C4BB4B42B1E7D75007981AA /* libHSsimplex-chat-5.4.0.6-DWi9o3X1dc6Jx2FZ4ew1fo-ghc8.10.7.a */, + 5C4BB4B52B1E7D75007981AA /* libHSsimplex-chat-5.4.0.6-DWi9o3X1dc6Jx2FZ4ew1fo.a */, ); path = Libraries; sourceTree = ""; diff --git a/apps/multiplatform/android/build.gradle.kts b/apps/multiplatform/android/build.gradle.kts index a35d3f519..a9909d2f6 100644 --- a/apps/multiplatform/android/build.gradle.kts +++ b/apps/multiplatform/android/build.gradle.kts @@ -12,7 +12,7 @@ android { defaultConfig { applicationId = "chat.simplex.app" - minSdkVersion(26) + minSdkVersion(28) targetSdkVersion(33) // !!! // skip version code after release to F-Droid, as it uses two version codes diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt index a84590fb8..cbe0ef7b1 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt @@ -41,9 +41,7 @@ class MainActivity: FragmentActivity() { ) } setContent { - SimpleXTheme { - AppScreen() - } + AppScreen() } SimplexApp.context.schedulePeriodicServiceRestartWorker() SimplexApp.context.schedulePeriodicWakeUp() diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt index 90696b79a..a345e6e48 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt @@ -32,7 +32,9 @@ class SimplexApp: Application(), LifecycleEventObserver { override fun onCreate() { super.onCreate() if (ProcessPhoenix.isPhoenixProcess(this)) { - return; + return + } else { + registerGlobalErrorHandler() } context = this initHaskell() diff --git a/apps/multiplatform/common/build.gradle.kts b/apps/multiplatform/common/build.gradle.kts index 55e03f620..4b0e38d8a 100644 --- a/apps/multiplatform/common/build.gradle.kts +++ b/apps/multiplatform/common/build.gradle.kts @@ -110,7 +110,7 @@ android { compileSdkVersion(34) sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") defaultConfig { - minSdkVersion(26) + minSdkVersion(28) targetSdkVersion(33) } compileOptions { diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt index ca497cbc5..96bb73911 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt @@ -8,10 +8,14 @@ import android.os.Build import android.view.* import android.view.inputmethod.InputMethodManager import android.widget.Toast +import androidx.activity.compose.setContent import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalView -import chat.simplex.common.views.helpers.KeyboardState +import chat.simplex.common.AppScreen +import chat.simplex.common.ui.theme.SimpleXTheme +import chat.simplex.common.views.helpers.* import androidx.compose.ui.platform.LocalContext as LocalContext1 +import chat.simplex.res.MR actual fun showToast(text: String, timeout: Long) = Toast.makeText(androidAppContext, text, Toast.LENGTH_SHORT).show() @@ -71,3 +75,37 @@ actual fun hideKeyboard(view: Any?) { } actual fun androidIsFinishingMainActivity(): Boolean = (mainActivity.get()?.isFinishing == true) + +actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler { + actual override fun uncaughtException(thread: Thread, e: Throwable) { + Log.e(TAG, "App crashed, thread name: " + thread.name + ", exception: " + e.stackTraceToString()) + if (ModalManager.start.hasModalsOpen()) { + ModalManager.start.closeModal() + } else if (chatModel.chatId.value != null) { + // Since no modals are open, the problem is probably in ChatView + chatModel.chatId.value = null + chatModel.chatItems.clear() + } else { + // ChatList, nothing to do. Maybe to show other view except ChatList + } + chatModel.activeCall.value?.let { + withBGApi { + chatModel.callManager.endCall(it) + } + } + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.app_was_crashed), + text = e.stackTraceToString() + ) + //mainActivity.get()?.recreate() + mainActivity.get()?.apply { + window + ?.decorView + ?.findViewById(android.R.id.content) + ?.removeViewAt(0) + setContent { + AppScreen() + } + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index d4062a1aa..4387adf95 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -42,9 +42,11 @@ data class SettingsViewState( @Composable fun AppScreen() { - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { - Surface(color = MaterialTheme.colors.background) { - MainScreen() + SimpleXTheme { + ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { + Surface(color = MaterialTheme.colors.background) { + MainScreen() + } } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 808b1938e..25c87d64a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -2,7 +2,6 @@ package chat.simplex.common.model import androidx.compose.material.* import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.SpanStyle @@ -68,6 +67,9 @@ object ChatModel { // set when app opened from external intent val clearOverlays = mutableStateOf(false) + // Only needed during onboarding when user skipped password setup (left as random password) + val desktopOnboardingRandomPassword = mutableStateOf(false) + // set when app is opened via contact or invitation URI val appOpenUrl = mutableStateOf(null) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt index c61d8564c..28ac357c2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt @@ -16,3 +16,11 @@ expect fun getKeyboardState(): State expect fun hideKeyboard(view: Any?) expect fun androidIsFinishingMainActivity(): Boolean + +fun registerGlobalErrorHandler() { + Thread.setDefaultUncaughtExceptionHandler(GlobalExceptionsHandler()) +} + +expect class GlobalExceptionsHandler(): Thread.UncaughtExceptionHandler { + override fun uncaughtException(thread: Thread, e: Throwable) +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt index a63c4c528..29d703329 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt @@ -207,12 +207,12 @@ fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: () -> Unit) { withApi { - chatModel.controller.apiCreateActiveUser( + chatModel.currentUser.value = chatModel.controller.apiCreateActiveUser( null, Profile(displayName.trim(), "", null) ) ?: return@withApi val onboardingStage = chatModel.controller.appPrefs.onboardingStage if (chatModel.users.isEmpty()) { - onboardingStage.set(if (appPlatform.isDesktop && chatModel.controller.appPrefs.initialRandomDBPassphrase.get()) { + onboardingStage.set(if (appPlatform.isDesktop && chatModel.controller.appPrefs.initialRandomDBPassphrase.get() && !chatModel.desktopOnboardingRandomPassword.value) { OnboardingStage.Step2_5_SetupDatabasePassphrase } else { OnboardingStage.Step3_CreateSimpleXAddress diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt index 10bbae6bf..ee9a8337a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt @@ -1,8 +1,11 @@ package chat.simplex.common.views.helpers import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -233,53 +236,71 @@ private fun alertTitle(title: String): (@Composable () -> Unit)? { @Composable private fun AlertContent(text: String?, hostDevice: Pair?, extraPadding: Boolean = false, content: @Composable (() -> Unit)) { - Column( - Modifier - .padding(bottom = if (appPlatform.isDesktop) DEFAULT_PADDING else DEFAULT_PADDING_HALF) - ) { - if (appPlatform.isDesktop) { - HostDeviceTitle(hostDevice, extraPadding = extraPadding) - } else { - Spacer(Modifier.size(DEFAULT_PADDING_HALF)) - } - CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) { - if (text != null) { - Text( - escapedHtmlToAnnotatedString(text, LocalDensity.current), - Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), - fontSize = 16.sp, - textAlign = TextAlign.Center, - color = MaterialTheme.colors.secondary - ) + BoxWithConstraints { + Column( + Modifier + .padding(bottom = if (appPlatform.isDesktop) DEFAULT_PADDING else DEFAULT_PADDING_HALF) + ) { + if (appPlatform.isDesktop) { + HostDeviceTitle(hostDevice, extraPadding = extraPadding) + } else { + Spacer(Modifier.size(DEFAULT_PADDING_HALF)) } + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) { + if (text != null) { + Column(Modifier.heightIn(max = this@BoxWithConstraints.maxHeight * 0.7f) + .verticalScroll(rememberScrollState()) + ) { + SelectionContainer { + Text( + escapedHtmlToAnnotatedString(text, LocalDensity.current), + Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), + fontSize = 16.sp, + textAlign = TextAlign.Center, + color = MaterialTheme.colors.secondary + ) + } + } + } + } + content() } - content() } } @Composable private fun AlertContent(text: AnnotatedString?, hostDevice: Pair?, extraPadding: Boolean = false, content: @Composable (() -> Unit)) { - Column( - Modifier - .padding(bottom = if (appPlatform.isDesktop) DEFAULT_PADDING else DEFAULT_PADDING_HALF) - ) { - if (appPlatform.isDesktop) { - HostDeviceTitle(hostDevice, extraPadding = extraPadding) - } else { - Spacer(Modifier.size(DEFAULT_PADDING_HALF)) - } - CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) { - if (text != null) { - Text( - text, - Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), - fontSize = 16.sp, - textAlign = TextAlign.Center, - color = MaterialTheme.colors.secondary - ) + BoxWithConstraints { + Column( + Modifier + .verticalScroll(rememberScrollState()) + .padding(bottom = if (appPlatform.isDesktop) DEFAULT_PADDING else DEFAULT_PADDING_HALF) + ) { + if (appPlatform.isDesktop) { + HostDeviceTitle(hostDevice, extraPadding = extraPadding) + } else { + Spacer(Modifier.size(DEFAULT_PADDING_HALF)) } + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) { + if (text != null) { + Column( + Modifier.heightIn(max = this@BoxWithConstraints.maxHeight * 0.7f) + .verticalScroll(rememberScrollState()) + ) { + SelectionContainer { + Text( + text, + Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), + fontSize = 16.sp, + textAlign = TextAlign.Center, + color = MaterialTheme.colors.secondary + ) + } + } + } + } + content() } - content() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt index 65098cea2..a6f0d2c9b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt @@ -19,8 +19,8 @@ import dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.* -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.* import chat.simplex.common.views.database.PassphraseStrength import chat.simplex.common.views.database.validKey import chat.simplex.res.MR @@ -123,6 +123,7 @@ fun DefaultConfigurableTextField( isValid: (String) -> Boolean, keyboardActions: KeyboardActions = KeyboardActions(), keyboardType: KeyboardType = KeyboardType.Text, + fontSize: TextUnit = 16.sp, dependsOn: State? = null, ) { var valid by remember { mutableStateOf(isValid(state.value.text)) } @@ -175,14 +176,14 @@ fun DefaultConfigurableTextField( textStyle = TextStyle.Default.copy( color = color, fontWeight = FontWeight.Normal, - fontSize = 16.sp + fontSize = fontSize ), interactionSource = interactionSource, decorationBox = @Composable { innerTextField -> TextFieldDefaults.TextFieldDecorationBox( value = state.value.text, innerTextField = innerTextField, - placeholder = { Text(placeholder, color = MaterialTheme.colors.secondary) }, + placeholder = { Text(placeholder, color = MaterialTheme.colors.secondary, fontSize = fontSize, maxLines = 1, overflow = TextOverflow.Ellipsis) }, singleLine = true, enabled = enabled, isError = !valid, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt index 904b2fc34..290bc2cd0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt @@ -20,6 +20,7 @@ fun ExposedDropDownSetting( values: List>, selection: State, textColor: Color = MaterialTheme.colors.secondary, + fontSize: TextUnit = 16.sp, label: String? = null, enabled: State = mutableStateOf(true), minWidth: Dp = 200.dp, @@ -43,7 +44,8 @@ fun ExposedDropDownSetting( Modifier.widthIn(max = maxWidth), maxLines = 1, overflow = TextOverflow.Ellipsis, - color = textColor + color = textColor, + fontSize = fontSize, ) Spacer(Modifier.size(12.dp)) Icon( @@ -69,6 +71,7 @@ fun ExposedDropDownSetting( maxLines = 1, overflow = TextOverflow.Ellipsis, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black, + fontSize = fontSize, ) } } @@ -91,6 +94,6 @@ fun ExposedDropDownSettingRow( onSelected: (T) -> Unit ) { SettingsActionItemWithContent(icon, title, iconColor = iconTint, disabled = !enabled.value) { - ExposedDropDownSetting(values, selection ,textColor, label, enabled, minWidth, maxWidth, onSelected) + ExposedDropDownSetting(values, selection ,textColor, label = label, enabled = enabled, minWidth = minWidth, maxWidth = maxWidth, onSelected = onSelected) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt index 03b3d6dbd..4e3b70405 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt @@ -73,7 +73,8 @@ private fun LinkAMobileLayout( } Box(Modifier.weight(0.7f)) { AddingMobileDevice(false, staleQrCode, connecting) { - if (chatModel.remoteHosts.isEmpty()) { + // currentRemoteHost will be set instantly but remoteHosts may be delayed + if (chatModel.remoteHosts.isEmpty() && chatModel.currentRemoteHost.value == null) { chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) } else { chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt index d9d012a50..c117e8997 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt @@ -1,10 +1,7 @@ package chat.simplex.common.views.onboarding import SectionBottomSpacer -import SectionItemView -import SectionItemViewSpaceBetween import SectionTextFooter -import SectionView import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions @@ -15,14 +12,12 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.* -import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.* import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import chat.simplex.common.model.* import chat.simplex.common.platform.* @@ -177,7 +172,10 @@ private fun SetupDatabasePassphraseLayout( } Spacer(Modifier.weight(1f)) - SkipButton(progressIndicator.value, nextStep) + SkipButton(progressIndicator.value) { + chatModel.desktopOnboardingRandomPassword.value = true + nextStep() + } SectionBottomSpacer() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt index c1c5d978c..a3218c961 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt @@ -174,8 +174,6 @@ private fun ConnectMobileViewLayout( sessionCode: String?, port: String?, staleQrCode: Boolean = false, - editEnabled: Boolean = false, - editClicked: () -> Unit = {}, refreshQrCode: () -> Unit = {}, UnderQrLayout: @Composable () -> Unit = {}, ) { @@ -201,16 +199,7 @@ private fun ConnectMobileViewLayout( } } SectionTextFooter(annotatedStringResource(MR.strings.open_on_mobile_and_scan_qr_code), textAlign = TextAlign.Center) - Row(verticalAlignment = Alignment.CenterVertically) { - SectionTextFooter(annotatedStringResource(MR.strings.waiting_for_mobile_to_connect_on_port, port), textAlign = TextAlign.Center) - if (editEnabled) { - Spacer(Modifier.width(4.dp)) - IconButton(editClicked, Modifier.size(16.dp)) { - Icon(painterResource(MR.images.ic_edit), stringResource(MR.strings.edit_verb), Modifier.size(16.dp), tint = MaterialTheme.colors.primary) - } - Spacer(Modifier.width(DEFAULT_PADDING)) - } - } + SectionTextFooter(annotatedStringResource(MR.strings.waiting_for_mobile_to_connect), textAlign = TextAlign.Center) UnderQrLayout() @@ -249,7 +238,9 @@ private fun ConnectMobileViewLayout( } } } - SectionBottomSpacer() + if (invitation != null) { + SectionBottomSpacer() + } } } @@ -275,10 +266,9 @@ private fun showAddingMobileDevice(connecting: MutableState) { @Composable fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState, connecting: MutableState, close: () -> Unit) { - val cachedR = remember { mutableStateOf(null) } + var cachedR by remember { mutableStateOf(null) } val customAddress = rememberSaveable { mutableStateOf(null) } val customPort = rememberSaveable { mutableStateOf(null) } - var editing by rememberSaveable { mutableStateOf(false) } val startRemoteHost = suspend { val r = chatModel.controller.startRemoteHost( rhId = null, @@ -287,9 +277,9 @@ fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState, c port = if (customPort.value != cachedR.port) customPort.value else cachedR.rh?.bindPort_ ) if (r != null) { - cachedR.value = r + cachedR = r connecting.value = true - customAddress.value = cachedR.address + customAddress.value = cachedR.addresses.firstOrNull() customPort.value = cachedR.port chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting } @@ -307,23 +297,20 @@ fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState, c val remoteDeviceName = pairing.value?.first?.hostDeviceName ConnectMobileViewLayout( title = if (!showTitle) null else if (cachedSessionCode == null) stringResource(MR.strings.link_a_mobile) else stringResource(MR.strings.verify_connection), - invitation = cachedR.invitation, + invitation = cachedR?.invitation, deviceName = remoteDeviceName, sessionCode = cachedSessionCode, - port = cachedR.value?.ctrlPort, - staleQrCode = staleQrCode.value || (cachedR.address != customAddress.value && customAddress.value != null) || (cachedR.port != customPort.value && customPort.value != null), - editEnabled = !editing && cachedR.addresses.isNotEmpty(), - editClicked = { editing = true }, + port = cachedR?.ctrlPort, + staleQrCode = staleQrCode.value || (cachedR.address != customAddress.value && customAddress.value != null) || cachedR.port != customPort.value, refreshQrCode = { withBGApi { if (chatController.stopRemoteHost(null)) { startRemoteHost() staleQrCode.value = false - editing = false } } }, - UnderQrLayout = { UnderQrLayout(editing, cachedR, customAddress, customPort) } + UnderQrLayout = { UnderQrLayout(cachedR, customAddress, customPort) } ) val oldRemoteHostId by remember { mutableStateOf(chatModel.currentRemoteHost.value?.remoteHostId) } LaunchedEffect(remember { chatModel.currentRemoteHost }.value) { @@ -353,10 +340,9 @@ fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState, c private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState) { ModalManager.start.showModalCloseable { close -> - val cachedR = remember { mutableStateOf(null) } + var cachedR by remember { mutableStateOf(null) } val customAddress = rememberSaveable { mutableStateOf(null) } val customPort = rememberSaveable { mutableStateOf(null) } - var editing by rememberSaveable { mutableStateOf(false) } val startRemoteHost = suspend { val r = chatModel.controller.startRemoteHost( rhId = rh.remoteHostId, @@ -365,9 +351,9 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState port = if (customPort.value != cachedR.port) customPort.value else cachedR.rh?.bindPort_ ?: rh.bindPort_ ) if (r != null) { - cachedR.value = r + cachedR = r connecting.value = true - customAddress.value = cachedR.address + customAddress.value = cachedR.addresses.firstOrNull() customPort.value = cachedR.port chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting } @@ -384,22 +370,19 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState } ConnectMobileViewLayout( title = if (cachedSessionCode == null) stringResource(MR.strings.scan_from_mobile) else stringResource(MR.strings.verify_connection), - invitation = cachedR.invitation, + invitation = cachedR?.invitation, deviceName = pairing.value?.first?.hostDeviceName ?: rh.hostDeviceName, sessionCode = cachedSessionCode, - port = cachedR.value?.ctrlPort, - staleQrCode = (cachedR.address != customAddress.value && customAddress.value != null) || (cachedR.port != customPort.value && customPort.value != null), - editEnabled = !editing && cachedR.addresses.isNotEmpty(), - editClicked = { editing = true }, + port = cachedR?.ctrlPort, + staleQrCode = (cachedR.address != customAddress.value && customAddress.value != null) || cachedR.port != customPort.value, refreshQrCode = { withBGApi { if (chatController.stopRemoteHost(rh.remoteHostId)) { startRemoteHost() - editing = false } } }, - UnderQrLayout = { UnderQrLayout(editing, cachedR, customAddress, customPort) } + UnderQrLayout = { UnderQrLayout(cachedR, customAddress, customPort) } ) LaunchedEffect(remember { chatModel.currentRemoteHost }.value) { if (cachedR.remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId == cachedR.remoteHostId) { @@ -453,48 +436,75 @@ private fun showConnectedMobileDevice(rh: RemoteHostInfo, disconnectHost: () -> } @Composable -private fun UnderQrLayout(editing: Boolean, cachedR: State, customAddress: MutableState, customPort: MutableState) { - if (editing) { - Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) { +private fun UnderQrLayout(cachedR: CR.RemoteHostStarted?, customAddress: MutableState, customPort: MutableState) { + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) { + if (cachedR.addresses.size > 1) { ExposedDropDownSetting( cachedR.addresses.map { it to it.address + " (${it.`interface`})" }, customAddress, textColor = MaterialTheme.colors.onBackground, + fontSize = 14.sp, minWidth = 250.dp, maxWidth = with(LocalDensity.current) { 250.sp.toDp() }, + enabled = remember { mutableStateOf(cachedR.addresses.size > 1) }, onSelected = { customAddress.value = it } ) - val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) { - mutableStateOf(TextFieldValue((customPort.value ?: cachedR.port!!).toString())) - } - Spacer(Modifier.width(DEFAULT_PADDING)) + } else { + Spacer(Modifier.width(10.dp)) + Text(customAddress.value?.address + " (${customAddress.value?.`interface`})", fontSize = 14.sp, color = MaterialTheme.colors.onBackground) + } + val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue((customPort.value ?: cachedR.port!!).toString())) + } + Spacer(Modifier.width(DEFAULT_PADDING)) + Box { DefaultConfigurableTextField( portUnsaved, - stringResource(MR.strings.port_verb), - modifier = Modifier.widthIn(max = 100.dp), - isValid = { validPort(it) && it.toInt() > 1023 }, + stringResource(MR.strings.random_port), + modifier = Modifier.widthIn(max = 132.dp), + isValid = { (validPort(it) && it.toInt() > 1023) || it.isBlank() }, keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done) }), keyboardType = KeyboardType.Number, + fontSize = 14.sp, ) - LaunchedEffect(Unit) { - snapshotFlow { portUnsaved.value.text } - .distinctUntilChanged() - .collect { - if (validPort(it) && it.toInt() > 1023) { - customPort.value = it.toInt() - } + if (validPort(portUnsaved.value.text) && portUnsaved.value.text.toInt() > 1023) { + Icon(painterResource(MR.images.ic_edit), stringResource(MR.strings.edit_verb), Modifier.padding(end = 56.dp).size(16.dp).align(Alignment.CenterEnd), tint = MaterialTheme.colors.secondary) + IconButton(::showOpenPortAlert, Modifier.align(Alignment.TopEnd).padding(top = 2.dp)) { + Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.primary) + } + } + } + LaunchedEffect(Unit) { + snapshotFlow { portUnsaved.value.text } + .distinctUntilChanged() + .collect { + if (validPort(it) && it.toInt() > 1023) { + customPort.value = it.toInt() + } else { + customPort.value = null } + } + } + KeyChangeEffect(customPort.value) { + if (customPort.value != null) { + portUnsaved.value = portUnsaved.value.copy(text = customPort.value.toString()) } } } } -private val State.rh: RemoteHostInfo? get() = value?.remoteHost_ -private val State.remoteHostId: Long? get() = value?.remoteHost_?.remoteHostId -private val State.invitation: String? get() = value?.invitation -private val State.address: RemoteCtrlAddress? get() = value?.localAddrs?.firstOrNull() -private val State.addresses: List get() = - (if (controller.appPrefs.developerTools.get()) value?.localAddrs else value?.localAddrs?.filterNot { it.address == "127.0.0.1" }) ?: emptyList() -private val State.port: Int? get() = value?.ctrlPort?.toIntOrNull() +private fun showOpenPortAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.open_port_in_firewall_title), + text = generalGetString(MR.strings.open_port_in_firewall_desc), + ) +} + +private val CR.RemoteHostStarted?.rh: RemoteHostInfo? get() = this?.remoteHost_ +private val CR.RemoteHostStarted?.remoteHostId: Long? get() = this?.remoteHost_?.remoteHostId +private val CR.RemoteHostStarted?.address: RemoteCtrlAddress? get() = this?.localAddrs?.firstOrNull() +private val CR.RemoteHostStarted?.addresses: List get() = + (if (controller.appPrefs.developerTools.get() || this?.localAddrs?.indexOfFirst { it.address == "127.0.0.1" } == 0) this?.localAddrs else this?.localAddrs?.filterNot { it.address == "127.0.0.1" }) ?: emptyList() +private val CR.RemoteHostStarted?.port: Int? get() = this?.ctrlPort?.toIntOrNull() diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index bfd61aa9e..e0b8f130d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -18,6 +18,7 @@ Opening database… Invalid file path You shared an invalid file path. Report the issue to the app developers. + View crashed connected @@ -1665,7 +1666,7 @@ Disconnect desktop? Only one device can work at the same time Use from desktop in mobile app and scan QR code.]]> - %s]]> + Waiting for mobile to connect: Bad desktop address Incompatible version Desktop app version %s is not compatible with this app. @@ -1693,6 +1694,9 @@ Not compatible! Refresh No connected mobile + Random + Open port in firewall + To allow a mobile app to connect to the desktop, open this port in your firewall, if you have it enabled Coming soon! diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml index 13b5cb239..1f8f75c12 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -1581,7 +1581,7 @@ Schnellerer Gruppenbeitritt und zuverlässigere Nachrichtenzustellung. Verknüpfte Mobiltelefone Dieser Gerätename - %s warten]]> + Auf die Mobiltelefonverbindung warten: Laden der Datei Zu einem Mobiltelefon verbinden Vom Desktop aus nutzen diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml index a9d315f68..efe9b1003 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml @@ -1486,7 +1486,7 @@ Bureau Connecté au bureau Ce nom d\'appareil - %s]]> + En attente d\'une connexion mobile: Chargement du fichier Connexion au bureau Appareils de bureau diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index 0883adfdb..9ed305da7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -1517,6 +1517,6 @@ - avvisa facoltativamente i contatti eliminati. \n- nomi del profilo con spazi. \n- e molto altro! - %s]]> + In attesa che il cellulare si connette: autore \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml index 0a258fdcb..6cef9571d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml @@ -1516,7 +1516,7 @@ \n- en meer! %s is verbroken]]> auteur - %s]]> + Wachten tot mobiel verbinding maakt: Automatisch verbinden Wachten op desktop… Desktop gevonden diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml index a9549080a..8f4872478 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml @@ -1496,7 +1496,7 @@ Szybsze dołączenie i bardziej niezawodne wiadomości. Połączone telefony Nazwa tego urządzenia - %s]]> + Oczekiwanie na połączenie telefonu: Ładowanie pliku Znaleziono komputer Urządzenia komputerowe diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml index dc5771316..f26f77c33 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -1601,7 +1601,7 @@ Проверить соединение Соединяться автоматически Ожидается подключение… - %s]]> + Ожидается подключение мобильного: Компьютер найден Несовместимая версия! автор diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml index 2f2c27e03..ed2b9986c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -1517,7 +1517,7 @@ - 可选择通知已删除的联系人。 \n- 带空格的个人资料名称。 \n- 以及更多! - %s 进行连接]]> + 正等待移动设备 进行连接: 作者 自动连接 等待桌面中… diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt index d6cc9c7bb..12bead366 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt @@ -9,30 +9,73 @@ import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.ComposeWindow import androidx.compose.ui.input.key.* import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.compose.ui.window.* import chat.simplex.common.model.ChatController import chat.simplex.common.model.ChatModel +import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH import chat.simplex.common.ui.theme.SimpleXTheme import chat.simplex.common.views.TerminalView -import chat.simplex.common.views.helpers.FileDialogChooser -import chat.simplex.common.views.helpers.escapedHtmlToAnnotatedString +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* import java.awt.event.WindowEvent import java.awt.event.WindowFocusListener import java.io.File +import kotlin.system.exitProcess val simplexWindowState = SimplexWindowState() -fun showApp() = application { +fun showApp() { + val closedByError = mutableStateOf(true) + while (closedByError.value) { + application(exitProcessOnExit = false) { + CompositionLocalProvider( + LocalWindowExceptionHandlerFactory provides WindowExceptionHandlerFactory { window -> + WindowExceptionHandler { e -> + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.app_was_crashed), + text = e.stackTraceToString() + ) + Log.e(TAG, "App crashed, thread name: " + Thread.currentThread().name + ", exception: " + e.stackTraceToString()) + window.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSING)) + closedByError.value = true + // If the left side of screen has open modal, it's probably caused the crash + if (ModalManager.start.hasModalsOpen()) { + ModalManager.start.closeModal() + } else if (ModalManager.center.hasModalsOpen() || ModalManager.end.hasModalsOpen()) { + ModalManager.center.closeModal() + ModalManager.end.closeModal() + // Better to not close fullscreen since it can contain passcode + } else { + // The last possible cause that can be closed + chatModel.chatId.value = null + chatModel.chatItems.clear() + } + chatModel.activeCall.value?.let { + withBGApi { + chatModel.callManager.endCall(it) + } + } + } + } + ) { + AppWindow(closedByError) + } + } + } + exitProcess(0) +} + +@Composable +private fun ApplicationScope.AppWindow(closedByError: MutableState) { // Creates file if not exists; comes with proper defaults val state = getStoredWindowState() - val windowState: WindowState = rememberWindowState( placement = WindowPlacement.Floating, width = state.width.dp, @@ -46,59 +89,62 @@ fun showApp() = application { windowState.size.width.value, windowState.size.height.value ) { - storeWindowState(WindowPositionSize( - x = windowState.position.x.value.toInt(), - y = windowState.position.y.value.toInt(), - width = windowState.size.width.value.toInt(), - height = windowState.size.height.value.toInt() - )) + storeWindowState( + WindowPositionSize( + x = windowState.position.x.value.toInt(), + y = windowState.position.y.value.toInt(), + width = windowState.size.width.value.toInt(), + height = windowState.size.height.value.toInt() + ) + ) } simplexWindowState.windowState = windowState // Reload all strings in all @Composable's after language change at runtime if (remember { ChatController.appPrefs.appLanguage.state }.value != "") { - Window(state = windowState, onCloseRequest = ::exitApplication, onKeyEvent = { + Window(state = windowState, onCloseRequest = { closedByError.value = false; exitApplication() }, onKeyEvent = { if (it.key == Key.Escape && it.type == KeyEventType.KeyUp) { simplexWindowState.backstack.lastOrNull()?.invoke() != null } else { false } }, title = "SimpleX") { - SimpleXTheme { - AppScreen() - if (simplexWindowState.openDialog.isAwaiting) { - FileDialogChooser( - title = "SimpleX", - isLoad = true, - params = simplexWindowState.openDialog.params, - onResult = { - simplexWindowState.openDialog.onResult(it.firstOrNull()) - } - ) - } + simplexWindowState.window = window + AppScreen() + if (simplexWindowState.openDialog.isAwaiting) { + FileDialogChooser( + title = "SimpleX", + isLoad = true, + params = simplexWindowState.openDialog.params, + onResult = { + simplexWindowState.openDialog.onResult(it.firstOrNull()) + } + ) + } - if (simplexWindowState.openMultipleDialog.isAwaiting) { - FileDialogChooser( - title = "SimpleX", - isLoad = true, - params = simplexWindowState.openMultipleDialog.params, - onResult = { - simplexWindowState.openMultipleDialog.onResult(it) - } - ) - } + if (simplexWindowState.openMultipleDialog.isAwaiting) { + FileDialogChooser( + title = "SimpleX", + isLoad = true, + params = simplexWindowState.openMultipleDialog.params, + onResult = { + simplexWindowState.openMultipleDialog.onResult(it) + } + ) + } - if (simplexWindowState.saveDialog.isAwaiting) { - FileDialogChooser( - title = "SimpleX", - isLoad = false, - params = simplexWindowState.saveDialog.params, - onResult = { simplexWindowState.saveDialog.onResult(it.firstOrNull()) } - ) - } - val toasts = remember { simplexWindowState.toasts } - val toast = toasts.firstOrNull() - if (toast != null) { + if (simplexWindowState.saveDialog.isAwaiting) { + FileDialogChooser( + title = "SimpleX", + isLoad = false, + params = simplexWindowState.saveDialog.params, + onResult = { simplexWindowState.saveDialog.onResult(it.firstOrNull()) } + ) + } + val toasts = remember { simplexWindowState.toasts } + val toast = toasts.firstOrNull() + if (toast != null) { + SimpleXTheme { Box(Modifier.fillMaxSize().padding(bottom = 20.dp), contentAlignment = Alignment.BottomCenter) { Text( escapedHtmlToAnnotatedString(toast.first, LocalDensity.current), @@ -107,11 +153,11 @@ fun showApp() = application { style = MaterialTheme.typography.body1 ) } - // Shows toast in insertion order with preferred delay per toast. New one will be shown once previous one expires - LaunchedEffect(toast, toasts.size) { - delay(toast.second) - simplexWindowState.toasts.removeFirst() - } + } + // Shows toast in insertion order with preferred delay per toast. New one will be shown once previous one expires + LaunchedEffect(toast, toasts.size) { + delay(toast.second) + simplexWindowState.toasts.removeFirst() } } var windowFocused by remember { simplexWindowState.windowFocused } @@ -160,6 +206,7 @@ class SimplexWindowState { val saveDialog = DialogState() val toasts = mutableStateListOf>() var windowFocused = mutableStateOf(true) + var window: ComposeWindow? = null } data class DialogParams( @@ -188,7 +235,5 @@ class DialogState { @Preview @Composable fun AppPreview() { - SimpleXTheme { - AppScreen() - } + AppScreen() } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt index 94d42ba79..b5a0e2e00 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt @@ -19,3 +19,9 @@ actual fun getKeyboardState(): State = remember { mutableStateOf( actual fun hideKeyboard(view: Any?) {} actual fun androidIsFinishingMainActivity(): Boolean = false + +actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler { + actual override fun uncaughtException(thread: Thread, e: Throwable) { + Log.e(TAG, "App crashed, thread name: " + thread.name + ", exception: " + e.stackTraceToString()) + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.desktop.kt index 83c438e4a..74c490445 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.desktop.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.Composable import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.SharedPreference import chat.simplex.common.model.User +import chat.simplex.common.platform.chatModel import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource @@ -14,7 +15,7 @@ import dev.icerock.moko.resources.compose.painterResource actual fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference, onclick: (() -> Unit)?) { if (user == null) { Row(horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING * 2.5f)) { - OnboardingActionButton(MR.strings.link_a_mobile, onboarding = if (controller.appPrefs.initialRandomDBPassphrase.get()) OnboardingStage.Step2_5_SetupDatabasePassphrase else OnboardingStage.LinkAMobile, true, icon = painterResource(MR.images.ic_smartphone_300), onclick = onclick) + OnboardingActionButton(MR.strings.link_a_mobile, onboarding = if (controller.appPrefs.initialRandomDBPassphrase.get() && !chatModel.desktopOnboardingRandomPassword.value) OnboardingStage.Step2_5_SetupDatabasePassphrase else OnboardingStage.LinkAMobile, true, icon = painterResource(MR.images.ic_smartphone_300), onclick = onclick) OnboardingActionButton(MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, true, icon = painterResource(MR.images.ic_desktop), onclick = onclick) } } else { diff --git a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt index 787e41fd8..e32b0ae79 100644 --- a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt +++ b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt @@ -3,10 +3,6 @@ package chat.simplex.desktop import chat.simplex.common.platform.* import chat.simplex.common.showApp import java.io.File -import java.nio.file.* -import java.nio.file.attribute.BasicFileAttributes -import java.nio.file.attribute.FileTime -import kotlin.io.path.setLastModifiedTime fun main() { initHaskell() diff --git a/cabal.project b/cabal.project index 777f094bc..0667b8304 100644 --- a/cabal.project +++ b/cabal.project @@ -11,7 +11,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: eaf5317834b069144b5f4897f9c79831983e54dd + tag: a860936072172e261480fa6bdd95203976e366b2 source-repository-package type: git diff --git a/docs/guide/making-connections.md b/docs/guide/making-connections.md index cf14883ae..2dd1f4bd7 100644 --- a/docs/guide/making-connections.md +++ b/docs/guide/making-connections.md @@ -17,7 +17,7 @@ Group Chat — Users have the option to create a secret group, share their conta You can [create an optional long term address](./app-settings.md#your-simplex-contact-address) for other people to connect with you. Unlike 1-time invitation links, these addresses can be used many times, that makes them good to share online, e.g. on social media platforms, or in email signatures. That helps more people discover SimpleX Chat, so please do it! -When people connect to you via this address, you will receive a connection request that you can accept or reject. You can configure an automatic acceptance of connection request and an automatic welcome message that will be sent to the new contacts. You can also share this address as part of your SimpleX profile, so group members can connect to you, and you contacts can share it with others - if this is something that you want. +When people connect to you via this address, you will receive a connection request that you can accept or reject. You can configure an automatic acceptance of connection request and an automatic welcome message that will be sent to the new contacts. You can also share this address as part of your SimpleX profile, so group members can connect to you, and your contacts can share it with others - if this is something that you want. If you start receiving too many requests via this address it is always safe to remove it – all the connections you created via this address will remain active, as this address is not used to deliver the messages. diff --git a/package.yaml b/package.yaml index 29b1be957..0f27f6035 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 5.4.0.6 +version: 5.4.0.7 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index b99146142..e34847138 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."eaf5317834b069144b5f4897f9c79831983e54dd" = "0jlic1q08mq9p9sgvigmc59r6x1r5fa1zsfqvvrwd97pwain36mj"; + "https://github.com/simplex-chat/simplexmq.git"."a860936072172e261480fa6bdd95203976e366b2" = "16rwnh5zzphmw8d8ypvps6xjvzbmf5ljr6zzy15gz2g0jyh7hd91"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/kazu-yamamoto/http2.git"."f5525b755ff2418e6e6ecc69e877363b0d0bcaeb" = "0fyx0047gvhm99ilp212mmz37j84cwrfnpmssib5dw363fyb88b6"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "0kiwhvml42g9anw4d2v0zd1fpc790pj9syg5x3ik4l97fnkbbwpp"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 05e1f106d..84ad7cd92 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: 5.4.0.6 +version: 5.4.0.7 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 35e673e8e..69f688740 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -189,6 +189,7 @@ mobileChatOpts dbFilePrefix dbKey = allowInstantFiles = True, autoAcceptFileSize = 0, muteNotifications = True, + markRead = False, maintenance = True }