Merge branch 'master-ghc8107' into master-android

This commit is contained in:
Evgeny Poberezkin 2023-12-07 15:12:49 +00:00
commit cdb3b6aafd
36 changed files with 364 additions and 234 deletions

View File

@ -11,20 +11,12 @@ import CoreImage.CIFilterBuiltins
struct MutableQRCode: View { struct MutableQRCode: View {
@Binding var uri: String @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 { var body: some View {
ZStack { QRCode(uri: uri, withLogo: withLogo, tintColor: tintColor)
if let image = image { .id("simplex-qrcode-view-for-\(uri)")
qrCodeImage(image)
}
}
.onAppear {
image = generateImage(uri)
}
.onChange(of: uri) { _ in
image = generateImage(uri)
}
} }
} }
@ -49,7 +41,7 @@ struct QRCode: View {
var withLogo: Bool = true var withLogo: Bool = true
var tintColor = UIColor(red: 0.023, green: 0.176, blue: 0.337, alpha: 1) var tintColor = UIColor(red: 0.023, green: 0.176, blue: 0.337, alpha: 1)
@State private var image: UIImage? = nil @State private var image: UIImage? = nil
@State private var makeScreenshotBinding: () -> Void = {} @State private var makeScreenshotFunc: () -> Void = {}
var body: some View { var body: some View {
ZStack { ZStack {
@ -70,18 +62,18 @@ struct QRCode: View {
} }
} }
.onAppear { .onAppear {
makeScreenshotBinding = { makeScreenshotFunc = {
let size = CGSizeMake(1024 / UIScreen.main.scale, 1024 / UIScreen.main.scale) 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) .frame(width: geo.size.width, height: geo.size.height)
} }
} }
.onTapGesture(perform: makeScreenshotBinding) .onTapGesture(perform: makeScreenshotFunc)
.onAppear { .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) .textSelection(.enabled)
} }
private func generateImage(_ uri: String) -> UIImage? { private func generateImage(_ uri: String, tintColor: UIColor) -> UIImage? {
let context = CIContext() let context = CIContext()
let filter = CIFilter.qrCodeGenerator() let filter = CIFilter.qrCodeGenerator()
filter.message = Data(uri.utf8) filter.message = Data(uri.utf8)
if let outputImage = filter.outputImage, if let outputImage = filter.outputImage,
let cgImage = context.createCGImage(outputImage, from: outputImage.extent) { let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
return UIImage(cgImage: cgImage) return UIImage(cgImage: cgImage).replaceColor(UIColor.black, tintColor)
} }
return nil return nil
} }

View File

@ -190,7 +190,8 @@ struct UserAddressView: View {
@ViewBuilder private func existingAddressView(_ userAddress: UserContactLink) -> some View { @ViewBuilder private func existingAddressView(_ userAddress: UserContactLink) -> some View {
Section { Section {
MutableQRCode(uri: Binding.constant(simplexChatLink(userAddress.connReqContact))) SimpleXLinkQRCode(uri: userAddress.connReqContact)
.id("simplex-contact-address-qrcode-\(userAddress.connReqContact)")
shareQRCodeButton(userAddress) shareQRCodeButton(userAddress)
if MFMailComposeViewController.canSendMail() { if MFMailComposeViewController.canSendMail() {
shareViaEmailButton(userAddress) shareViaEmailButton(userAddress)

View File

@ -43,6 +43,11 @@
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */; }; 5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */; };
5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */; }; 5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */; };
5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B3B09285FB130003915F2 /* DatabaseView.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 */; }; 5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; };
5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A91E283AD0E400C4E99E /* CallManager.swift */; }; 5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A91E283AD0E400C4E99E /* CallManager.swift */; };
5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A920283CCCB700C4E99E /* IncomingCallView.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 */; }; 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, ); }; }; 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 */; }; 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 */; }; 5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; };
5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; }; 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, ); }; }; 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 = "<group>"; }; 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettings.swift; sourceTree = "<group>"; };
5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = "<group>"; }; 5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = "<group>"; };
5C4B3B09285FB130003915F2 /* DatabaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseView.swift; sourceTree = "<group>"; }; 5C4B3B09285FB130003915F2 /* DatabaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseView.swift; sourceTree = "<group>"; };
5C4BB4B32B1E7D75007981AA /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
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 = "<group>"; };
5C4BB4B52B1E7D75007981AA /* libHSsimplex-chat-5.4.0.6-DWi9o3X1dc6Jx2FZ4ew1fo.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.6-DWi9o3X1dc6Jx2FZ4ew1fo.a"; sourceTree = "<group>"; };
5C4BB4B62B1E7D75007981AA /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C4BB4B72B1E7D75007981AA /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = "<group>"; }; 5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = "<group>"; };
5C55A91E283AD0E400C4E99E /* CallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = "<group>"; }; 5C55A91E283AD0E400C4E99E /* CallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = "<group>"; };
5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = "<group>"; }; 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = "<group>"; };
@ -408,11 +413,6 @@
5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanToConnectView.swift; sourceTree = "<group>"; }; 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanToConnectView.swift; sourceTree = "<group>"; };
5CD67B8D2B0E858A00C510B1 /* hs_init.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hs_init.h; sourceTree = "<group>"; }; 5CD67B8D2B0E858A00C510B1 /* hs_init.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hs_init.h; sourceTree = "<group>"; };
5CD67B8E2B0E858A00C510B1 /* hs_init.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = hs_init.c; sourceTree = "<group>"; }; 5CD67B8E2B0E858A00C510B1 /* hs_init.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = hs_init.c; sourceTree = "<group>"; };
5CD67B9B2B120ADF00C510B1 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5CD67B9C2B120ADF00C510B1 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5CD67B9D2B120ADF00C510B1 /* libHSsimplex-chat-5.4.0.6-9DfazyElTA72omjHp0C93u.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.6-9DfazyElTA72omjHp0C93u.a"; sourceTree = "<group>"; };
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 = "<group>"; };
5CD67B9F2B120ADF00C510B1 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5CDCAD452818589900503DA2 /* SimpleX NSE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX NSE.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; 5CDCAD472818589900503DA2 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
5CDCAD492818589900503DA2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 5CDCAD492818589900503DA2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -521,13 +521,13 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
5CD67BA12B120ADF00C510B1 /* libgmpxx.a in Frameworks */,
5CD67BA22B120ADF00C510B1 /* libHSsimplex-chat-5.4.0.6-9DfazyElTA72omjHp0C93u.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5CD67BA02B120ADF00C510B1 /* libgmp.a in Frameworks */, 5C4BB4BB2B1E7D75007981AA /* libffi.a in Frameworks */,
5CD67BA42B120ADF00C510B1 /* libffi.a in Frameworks */, 5C4BB4BA2B1E7D75007981AA /* libHSsimplex-chat-5.4.0.6-DWi9o3X1dc6Jx2FZ4ew1fo.a in Frameworks */,
5CD67BA32B120ADF00C510B1 /* libHSsimplex-chat-5.4.0.6-9DfazyElTA72omjHp0C93u-ghc8.10.7.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 */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
5C4BB4BC2B1E7D75007981AA /* libgmpxx.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -589,11 +589,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = { 5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5CD67B9F2B120ADF00C510B1 /* libffi.a */, 5C4BB4B62B1E7D75007981AA /* libffi.a */,
5CD67B9B2B120ADF00C510B1 /* libgmp.a */, 5C4BB4B32B1E7D75007981AA /* libgmp.a */,
5CD67B9C2B120ADF00C510B1 /* libgmpxx.a */, 5C4BB4B72B1E7D75007981AA /* libgmpxx.a */,
5CD67B9E2B120ADF00C510B1 /* libHSsimplex-chat-5.4.0.6-9DfazyElTA72omjHp0C93u-ghc8.10.7.a */, 5C4BB4B42B1E7D75007981AA /* libHSsimplex-chat-5.4.0.6-DWi9o3X1dc6Jx2FZ4ew1fo-ghc8.10.7.a */,
5CD67B9D2B120ADF00C510B1 /* libHSsimplex-chat-5.4.0.6-9DfazyElTA72omjHp0C93u.a */, 5C4BB4B52B1E7D75007981AA /* libHSsimplex-chat-5.4.0.6-DWi9o3X1dc6Jx2FZ4ew1fo.a */,
); );
path = Libraries; path = Libraries;
sourceTree = "<group>"; sourceTree = "<group>";

View File

@ -12,7 +12,7 @@ android {
defaultConfig { defaultConfig {
applicationId = "chat.simplex.app" applicationId = "chat.simplex.app"
minSdkVersion(26) minSdkVersion(28)
targetSdkVersion(33) targetSdkVersion(33)
// !!! // !!!
// skip version code after release to F-Droid, as it uses two version codes // skip version code after release to F-Droid, as it uses two version codes

View File

@ -41,9 +41,7 @@ class MainActivity: FragmentActivity() {
) )
} }
setContent { setContent {
SimpleXTheme { AppScreen()
AppScreen()
}
} }
SimplexApp.context.schedulePeriodicServiceRestartWorker() SimplexApp.context.schedulePeriodicServiceRestartWorker()
SimplexApp.context.schedulePeriodicWakeUp() SimplexApp.context.schedulePeriodicWakeUp()

View File

@ -32,7 +32,9 @@ class SimplexApp: Application(), LifecycleEventObserver {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
if (ProcessPhoenix.isPhoenixProcess(this)) { if (ProcessPhoenix.isPhoenixProcess(this)) {
return; return
} else {
registerGlobalErrorHandler()
} }
context = this context = this
initHaskell() initHaskell()

View File

@ -110,7 +110,7 @@ android {
compileSdkVersion(34) compileSdkVersion(34)
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
defaultConfig { defaultConfig {
minSdkVersion(26) minSdkVersion(28)
targetSdkVersion(33) targetSdkVersion(33)
} }
compileOptions { compileOptions {

View File

@ -8,10 +8,14 @@ import android.os.Build
import android.view.* import android.view.*
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalView 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 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() 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 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<ViewGroup>(android.R.id.content)
?.removeViewAt(0)
setContent {
AppScreen()
}
}
}
}

View File

@ -42,9 +42,11 @@ data class SettingsViewState(
@Composable @Composable
fun AppScreen() { fun AppScreen() {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { SimpleXTheme {
Surface(color = MaterialTheme.colors.background) { ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
MainScreen() Surface(color = MaterialTheme.colors.background) {
MainScreen()
}
} }
} }
} }

View File

@ -2,7 +2,6 @@ package chat.simplex.common.model
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
@ -68,6 +67,9 @@ object ChatModel {
// set when app opened from external intent // set when app opened from external intent
val clearOverlays = mutableStateOf<Boolean>(false) val clearOverlays = mutableStateOf<Boolean>(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 // set when app is opened via contact or invitation URI
val appOpenUrl = mutableStateOf<URI?>(null) val appOpenUrl = mutableStateOf<URI?>(null)

View File

@ -16,3 +16,11 @@ expect fun getKeyboardState(): State<KeyboardState>
expect fun hideKeyboard(view: Any?) expect fun hideKeyboard(view: Any?)
expect fun androidIsFinishingMainActivity(): Boolean expect fun androidIsFinishingMainActivity(): Boolean
fun registerGlobalErrorHandler() {
Thread.setDefaultUncaughtExceptionHandler(GlobalExceptionsHandler())
}
expect class GlobalExceptionsHandler(): Thread.UncaughtExceptionHandler {
override fun uncaughtException(thread: Thread, e: Throwable)
}

View File

@ -207,12 +207,12 @@ fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: ()
fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: () -> Unit) { fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: () -> Unit) {
withApi { withApi {
chatModel.controller.apiCreateActiveUser( chatModel.currentUser.value = chatModel.controller.apiCreateActiveUser(
null, Profile(displayName.trim(), "", null) null, Profile(displayName.trim(), "", null)
) ?: return@withApi ) ?: return@withApi
val onboardingStage = chatModel.controller.appPrefs.onboardingStage val onboardingStage = chatModel.controller.appPrefs.onboardingStage
if (chatModel.users.isEmpty()) { 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 OnboardingStage.Step2_5_SetupDatabasePassphrase
} else { } else {
OnboardingStage.Step3_CreateSimpleXAddress OnboardingStage.Step3_CreateSimpleXAddress

View File

@ -1,8 +1,11 @@
package chat.simplex.common.views.helpers package chat.simplex.common.views.helpers
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape 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.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -233,53 +236,71 @@ private fun alertTitle(title: String): (@Composable () -> Unit)? {
@Composable @Composable
private fun AlertContent(text: String?, hostDevice: Pair<Long?, String>?, extraPadding: Boolean = false, content: @Composable (() -> Unit)) { private fun AlertContent(text: String?, hostDevice: Pair<Long?, String>?, extraPadding: Boolean = false, content: @Composable (() -> Unit)) {
Column( BoxWithConstraints {
Modifier Column(
.padding(bottom = if (appPlatform.isDesktop) DEFAULT_PADDING else DEFAULT_PADDING_HALF) Modifier
) { .padding(bottom = if (appPlatform.isDesktop) DEFAULT_PADDING else DEFAULT_PADDING_HALF)
if (appPlatform.isDesktop) { ) {
HostDeviceTitle(hostDevice, extraPadding = extraPadding) if (appPlatform.isDesktop) {
} else { HostDeviceTitle(hostDevice, extraPadding = extraPadding)
Spacer(Modifier.size(DEFAULT_PADDING_HALF)) } 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
)
} }
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 @Composable
private fun AlertContent(text: AnnotatedString?, hostDevice: Pair<Long?, String>?, extraPadding: Boolean = false, content: @Composable (() -> Unit)) { private fun AlertContent(text: AnnotatedString?, hostDevice: Pair<Long?, String>?, extraPadding: Boolean = false, content: @Composable (() -> Unit)) {
Column( BoxWithConstraints {
Modifier Column(
.padding(bottom = if (appPlatform.isDesktop) DEFAULT_PADDING else DEFAULT_PADDING_HALF) Modifier
) { .verticalScroll(rememberScrollState())
if (appPlatform.isDesktop) { .padding(bottom = if (appPlatform.isDesktop) DEFAULT_PADDING else DEFAULT_PADDING_HALF)
HostDeviceTitle(hostDevice, extraPadding = extraPadding) ) {
} else { if (appPlatform.isDesktop) {
Spacer(Modifier.size(DEFAULT_PADDING_HALF)) HostDeviceTitle(hostDevice, extraPadding = extraPadding)
} } else {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) { Spacer(Modifier.size(DEFAULT_PADDING_HALF))
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
)
} }
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()
} }
} }

View File

@ -19,8 +19,8 @@ import dev.icerock.moko.resources.compose.painterResource
import androidx.compose.ui.text.* import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.* import androidx.compose.ui.text.input.*
import androidx.compose.ui.unit.dp import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.*
import chat.simplex.common.views.database.PassphraseStrength import chat.simplex.common.views.database.PassphraseStrength
import chat.simplex.common.views.database.validKey import chat.simplex.common.views.database.validKey
import chat.simplex.res.MR import chat.simplex.res.MR
@ -123,6 +123,7 @@ fun DefaultConfigurableTextField(
isValid: (String) -> Boolean, isValid: (String) -> Boolean,
keyboardActions: KeyboardActions = KeyboardActions(), keyboardActions: KeyboardActions = KeyboardActions(),
keyboardType: KeyboardType = KeyboardType.Text, keyboardType: KeyboardType = KeyboardType.Text,
fontSize: TextUnit = 16.sp,
dependsOn: State<Any?>? = null, dependsOn: State<Any?>? = null,
) { ) {
var valid by remember { mutableStateOf(isValid(state.value.text)) } var valid by remember { mutableStateOf(isValid(state.value.text)) }
@ -175,14 +176,14 @@ fun DefaultConfigurableTextField(
textStyle = TextStyle.Default.copy( textStyle = TextStyle.Default.copy(
color = color, color = color,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 16.sp fontSize = fontSize
), ),
interactionSource = interactionSource, interactionSource = interactionSource,
decorationBox = @Composable { innerTextField -> decorationBox = @Composable { innerTextField ->
TextFieldDefaults.TextFieldDecorationBox( TextFieldDefaults.TextFieldDecorationBox(
value = state.value.text, value = state.value.text,
innerTextField = innerTextField, 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, singleLine = true,
enabled = enabled, enabled = enabled,
isError = !valid, isError = !valid,

View File

@ -20,6 +20,7 @@ fun <T> ExposedDropDownSetting(
values: List<Pair<T, String>>, values: List<Pair<T, String>>,
selection: State<T>, selection: State<T>,
textColor: Color = MaterialTheme.colors.secondary, textColor: Color = MaterialTheme.colors.secondary,
fontSize: TextUnit = 16.sp,
label: String? = null, label: String? = null,
enabled: State<Boolean> = mutableStateOf(true), enabled: State<Boolean> = mutableStateOf(true),
minWidth: Dp = 200.dp, minWidth: Dp = 200.dp,
@ -43,7 +44,8 @@ fun <T> ExposedDropDownSetting(
Modifier.widthIn(max = maxWidth), Modifier.widthIn(max = maxWidth),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
color = textColor color = textColor,
fontSize = fontSize,
) )
Spacer(Modifier.size(12.dp)) Spacer(Modifier.size(12.dp))
Icon( Icon(
@ -69,6 +71,7 @@ fun <T> ExposedDropDownSetting(
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
fontSize = fontSize,
) )
} }
} }
@ -91,6 +94,6 @@ fun <T> ExposedDropDownSettingRow(
onSelected: (T) -> Unit onSelected: (T) -> Unit
) { ) {
SettingsActionItemWithContent(icon, title, iconColor = iconTint, disabled = !enabled.value) { 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)
} }
} }

View File

@ -73,7 +73,8 @@ private fun LinkAMobileLayout(
} }
Box(Modifier.weight(0.7f)) { Box(Modifier.weight(0.7f)) {
AddingMobileDevice(false, staleQrCode, connecting) { 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) chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
} else { } else {
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete) chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete)

View File

@ -1,10 +1,7 @@
package chat.simplex.common.views.onboarding package chat.simplex.common.views.onboarding
import SectionBottomSpacer import SectionBottomSpacer
import SectionItemView
import SectionItemViewSpaceBetween
import SectionTextFooter import SectionTextFooter
import SectionView
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.* import androidx.compose.ui.focus.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.* import androidx.compose.ui.input.key.*
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
@ -177,7 +172,10 @@ private fun SetupDatabasePassphraseLayout(
} }
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
SkipButton(progressIndicator.value, nextStep) SkipButton(progressIndicator.value) {
chatModel.desktopOnboardingRandomPassword.value = true
nextStep()
}
SectionBottomSpacer() SectionBottomSpacer()
} }

View File

@ -174,8 +174,6 @@ private fun ConnectMobileViewLayout(
sessionCode: String?, sessionCode: String?,
port: String?, port: String?,
staleQrCode: Boolean = false, staleQrCode: Boolean = false,
editEnabled: Boolean = false,
editClicked: () -> Unit = {},
refreshQrCode: () -> Unit = {}, refreshQrCode: () -> Unit = {},
UnderQrLayout: @Composable () -> 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) 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), textAlign = TextAlign.Center)
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))
}
}
UnderQrLayout() UnderQrLayout()
@ -249,7 +238,9 @@ private fun ConnectMobileViewLayout(
} }
} }
} }
SectionBottomSpacer() if (invitation != null) {
SectionBottomSpacer()
}
} }
} }
@ -275,10 +266,9 @@ private fun showAddingMobileDevice(connecting: MutableState<Boolean>) {
@Composable @Composable
fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState<Boolean>, connecting: MutableState<Boolean>, close: () -> Unit) { fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState<Boolean>, connecting: MutableState<Boolean>, close: () -> Unit) {
val cachedR = remember { mutableStateOf<CR.RemoteHostStarted?>(null) } var cachedR by remember { mutableStateOf<CR.RemoteHostStarted?>(null) }
val customAddress = rememberSaveable { mutableStateOf<RemoteCtrlAddress?>(null) } val customAddress = rememberSaveable { mutableStateOf<RemoteCtrlAddress?>(null) }
val customPort = rememberSaveable { mutableStateOf<Int?>(null) } val customPort = rememberSaveable { mutableStateOf<Int?>(null) }
var editing by rememberSaveable { mutableStateOf(false) }
val startRemoteHost = suspend { val startRemoteHost = suspend {
val r = chatModel.controller.startRemoteHost( val r = chatModel.controller.startRemoteHost(
rhId = null, rhId = null,
@ -287,9 +277,9 @@ fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState<Boolean>, c
port = if (customPort.value != cachedR.port) customPort.value else cachedR.rh?.bindPort_ port = if (customPort.value != cachedR.port) customPort.value else cachedR.rh?.bindPort_
) )
if (r != null) { if (r != null) {
cachedR.value = r cachedR = r
connecting.value = true connecting.value = true
customAddress.value = cachedR.address customAddress.value = cachedR.addresses.firstOrNull()
customPort.value = cachedR.port customPort.value = cachedR.port
chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting
} }
@ -307,23 +297,20 @@ fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState<Boolean>, c
val remoteDeviceName = pairing.value?.first?.hostDeviceName val remoteDeviceName = pairing.value?.first?.hostDeviceName
ConnectMobileViewLayout( ConnectMobileViewLayout(
title = if (!showTitle) null else if (cachedSessionCode == null) stringResource(MR.strings.link_a_mobile) else stringResource(MR.strings.verify_connection), 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, deviceName = remoteDeviceName,
sessionCode = cachedSessionCode, sessionCode = cachedSessionCode,
port = cachedR.value?.ctrlPort, port = cachedR?.ctrlPort,
staleQrCode = staleQrCode.value || (cachedR.address != customAddress.value && customAddress.value != null) || (cachedR.port != customPort.value && customPort.value != null), staleQrCode = staleQrCode.value || (cachedR.address != customAddress.value && customAddress.value != null) || cachedR.port != customPort.value,
editEnabled = !editing && cachedR.addresses.isNotEmpty(),
editClicked = { editing = true },
refreshQrCode = { refreshQrCode = {
withBGApi { withBGApi {
if (chatController.stopRemoteHost(null)) { if (chatController.stopRemoteHost(null)) {
startRemoteHost() startRemoteHost()
staleQrCode.value = false 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) } val oldRemoteHostId by remember { mutableStateOf(chatModel.currentRemoteHost.value?.remoteHostId) }
LaunchedEffect(remember { chatModel.currentRemoteHost }.value) { LaunchedEffect(remember { chatModel.currentRemoteHost }.value) {
@ -353,10 +340,9 @@ fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState<Boolean>, c
private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState<Boolean>) { private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState<Boolean>) {
ModalManager.start.showModalCloseable { close -> ModalManager.start.showModalCloseable { close ->
val cachedR = remember { mutableStateOf<CR.RemoteHostStarted?>(null) } var cachedR by remember { mutableStateOf<CR.RemoteHostStarted?>(null) }
val customAddress = rememberSaveable { mutableStateOf<RemoteCtrlAddress?>(null) } val customAddress = rememberSaveable { mutableStateOf<RemoteCtrlAddress?>(null) }
val customPort = rememberSaveable { mutableStateOf<Int?>(null) } val customPort = rememberSaveable { mutableStateOf<Int?>(null) }
var editing by rememberSaveable { mutableStateOf(false) }
val startRemoteHost = suspend { val startRemoteHost = suspend {
val r = chatModel.controller.startRemoteHost( val r = chatModel.controller.startRemoteHost(
rhId = rh.remoteHostId, 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_ port = if (customPort.value != cachedR.port) customPort.value else cachedR.rh?.bindPort_ ?: rh.bindPort_
) )
if (r != null) { if (r != null) {
cachedR.value = r cachedR = r
connecting.value = true connecting.value = true
customAddress.value = cachedR.address customAddress.value = cachedR.addresses.firstOrNull()
customPort.value = cachedR.port customPort.value = cachedR.port
chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting
} }
@ -384,22 +370,19 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState
} }
ConnectMobileViewLayout( ConnectMobileViewLayout(
title = if (cachedSessionCode == null) stringResource(MR.strings.scan_from_mobile) else stringResource(MR.strings.verify_connection), 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, deviceName = pairing.value?.first?.hostDeviceName ?: rh.hostDeviceName,
sessionCode = cachedSessionCode, sessionCode = cachedSessionCode,
port = cachedR.value?.ctrlPort, port = cachedR?.ctrlPort,
staleQrCode = (cachedR.address != customAddress.value && customAddress.value != null) || (cachedR.port != customPort.value && customPort.value != null), staleQrCode = (cachedR.address != customAddress.value && customAddress.value != null) || cachedR.port != customPort.value,
editEnabled = !editing && cachedR.addresses.isNotEmpty(),
editClicked = { editing = true },
refreshQrCode = { refreshQrCode = {
withBGApi { withBGApi {
if (chatController.stopRemoteHost(rh.remoteHostId)) { if (chatController.stopRemoteHost(rh.remoteHostId)) {
startRemoteHost() startRemoteHost()
editing = false
} }
} }
}, },
UnderQrLayout = { UnderQrLayout(editing, cachedR, customAddress, customPort) } UnderQrLayout = { UnderQrLayout(cachedR, customAddress, customPort) }
) )
LaunchedEffect(remember { chatModel.currentRemoteHost }.value) { LaunchedEffect(remember { chatModel.currentRemoteHost }.value) {
if (cachedR.remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId == cachedR.remoteHostId) { if (cachedR.remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId == cachedR.remoteHostId) {
@ -453,48 +436,75 @@ private fun showConnectedMobileDevice(rh: RemoteHostInfo, disconnectHost: () ->
} }
@Composable @Composable
private fun UnderQrLayout(editing: Boolean, cachedR: State<CR.RemoteHostStarted?>, customAddress: MutableState<RemoteCtrlAddress?>, customPort: MutableState<Int?>) { private fun UnderQrLayout(cachedR: CR.RemoteHostStarted?, customAddress: MutableState<RemoteCtrlAddress?>, customPort: MutableState<Int?>) {
if (editing) { Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) {
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) { if (cachedR.addresses.size > 1) {
ExposedDropDownSetting( ExposedDropDownSetting(
cachedR.addresses.map { it to it.address + " (${it.`interface`})" }, cachedR.addresses.map { it to it.address + " (${it.`interface`})" },
customAddress, customAddress,
textColor = MaterialTheme.colors.onBackground, textColor = MaterialTheme.colors.onBackground,
fontSize = 14.sp,
minWidth = 250.dp, minWidth = 250.dp,
maxWidth = with(LocalDensity.current) { 250.sp.toDp() }, maxWidth = with(LocalDensity.current) { 250.sp.toDp() },
enabled = remember { mutableStateOf(cachedR.addresses.size > 1) },
onSelected = { onSelected = {
customAddress.value = it customAddress.value = it
} }
) )
val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) { } else {
mutableStateOf(TextFieldValue((customPort.value ?: cachedR.port!!).toString())) Spacer(Modifier.width(10.dp))
} Text(customAddress.value?.address + " (${customAddress.value?.`interface`})", fontSize = 14.sp, color = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING)) }
val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue((customPort.value ?: cachedR.port!!).toString()))
}
Spacer(Modifier.width(DEFAULT_PADDING))
Box {
DefaultConfigurableTextField( DefaultConfigurableTextField(
portUnsaved, portUnsaved,
stringResource(MR.strings.port_verb), stringResource(MR.strings.random_port),
modifier = Modifier.widthIn(max = 100.dp), modifier = Modifier.widthIn(max = 132.dp),
isValid = { validPort(it) && it.toInt() > 1023 }, isValid = { (validPort(it) && it.toInt() > 1023) || it.isBlank() },
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done) }), keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done) }),
keyboardType = KeyboardType.Number, keyboardType = KeyboardType.Number,
fontSize = 14.sp,
) )
LaunchedEffect(Unit) { if (validPort(portUnsaved.value.text) && portUnsaved.value.text.toInt() > 1023) {
snapshotFlow { portUnsaved.value.text } 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)
.distinctUntilChanged() IconButton(::showOpenPortAlert, Modifier.align(Alignment.TopEnd).padding(top = 2.dp)) {
.collect { Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.primary)
if (validPort(it) && it.toInt() > 1023) { }
customPort.value = it.toInt() }
} }
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<CR.RemoteHostStarted?>.rh: RemoteHostInfo? get() = value?.remoteHost_ private fun showOpenPortAlert() {
private val State<CR.RemoteHostStarted?>.remoteHostId: Long? get() = value?.remoteHost_?.remoteHostId AlertManager.shared.showAlertMsg(
private val State<CR.RemoteHostStarted?>.invitation: String? get() = value?.invitation title = generalGetString(MR.strings.open_port_in_firewall_title),
private val State<CR.RemoteHostStarted?>.address: RemoteCtrlAddress? get() = value?.localAddrs?.firstOrNull() text = generalGetString(MR.strings.open_port_in_firewall_desc),
private val State<CR.RemoteHostStarted?>.addresses: List<RemoteCtrlAddress> get() = )
(if (controller.appPrefs.developerTools.get()) value?.localAddrs else value?.localAddrs?.filterNot { it.address == "127.0.0.1" }) ?: emptyList() }
private val State<CR.RemoteHostStarted?>.port: Int? get() = value?.ctrlPort?.toIntOrNull()
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<RemoteCtrlAddress> 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()

View File

@ -18,6 +18,7 @@
<string name="opening_database">Opening database…</string> <string name="opening_database">Opening database…</string>
<string name="non_content_uri_alert_title">Invalid file path</string> <string name="non_content_uri_alert_title">Invalid file path</string>
<string name="non_content_uri_alert_text">You shared an invalid file path. Report the issue to the app developers.</string> <string name="non_content_uri_alert_text">You shared an invalid file path. Report the issue to the app developers.</string>
<string name="app_was_crashed">View crashed</string>
<!-- Server info - ChatModel.kt --> <!-- Server info - ChatModel.kt -->
<string name="server_connected">connected</string> <string name="server_connected">connected</string>
@ -1665,7 +1666,7 @@
<string name="disconnect_desktop_question">Disconnect desktop?</string> <string name="disconnect_desktop_question">Disconnect desktop?</string>
<string name="only_one_device_can_work_at_the_same_time">Only one device can work at the same time</string> <string name="only_one_device_can_work_at_the_same_time">Only one device can work at the same time</string>
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[Open <i>Use from desktop</i> in mobile app and scan QR code.]]></string> <string name="open_on_mobile_and_scan_qr_code"><![CDATA[Open <i>Use from desktop</i> in mobile app and scan QR code.]]></string>
<string name="waiting_for_mobile_to_connect_on_port"><![CDATA[Waiting for mobile to connect on port <i>%s</i>]]></string> <string name="waiting_for_mobile_to_connect">Waiting for mobile to connect:</string>
<string name="bad_desktop_address">Bad desktop address</string> <string name="bad_desktop_address">Bad desktop address</string>
<string name="desktop_incompatible_version">Incompatible version</string> <string name="desktop_incompatible_version">Incompatible version</string>
<string name="desktop_app_version_is_incompatible">Desktop app version %s is not compatible with this app.</string> <string name="desktop_app_version_is_incompatible">Desktop app version %s is not compatible with this app.</string>
@ -1693,6 +1694,9 @@
<string name="not_compatible">Not compatible!</string> <string name="not_compatible">Not compatible!</string>
<string name="refresh_qr_code">Refresh</string> <string name="refresh_qr_code">Refresh</string>
<string name="no_connected_mobile">No connected mobile</string> <string name="no_connected_mobile">No connected mobile</string>
<string name="random_port">Random</string>
<string name="open_port_in_firewall_title">Open port in firewall</string>
<string name="open_port_in_firewall_desc">To allow a mobile app to connect to the desktop, open this port in your firewall, if you have it enabled</string>
<!-- Under development --> <!-- Under development -->
<string name="in_developing_title">Coming soon!</string> <string name="in_developing_title">Coming soon!</string>

View File

@ -1581,7 +1581,7 @@
<string name="v5_4_better_groups_descr">Schnellerer Gruppenbeitritt und zuverlässigere Nachrichtenzustellung.</string> <string name="v5_4_better_groups_descr">Schnellerer Gruppenbeitritt und zuverlässigere Nachrichtenzustellung.</string>
<string name="linked_mobiles">Verknüpfte Mobiltelefone</string> <string name="linked_mobiles">Verknüpfte Mobiltelefone</string>
<string name="this_device_name">Dieser Gerätename</string> <string name="this_device_name">Dieser Gerätename</string>
<string name="waiting_for_mobile_to_connect_on_port"><![CDATA[Auf die Mobiltelefonverbindung über Port <i>%s</i> warten]]></string> <string name="waiting_for_mobile_to_connect">Auf die Mobiltelefonverbindung warten:</string>
<string name="loading_remote_file_title">Laden der Datei</string> <string name="loading_remote_file_title">Laden der Datei</string>
<string name="link_a_mobile">Zu einem Mobiltelefon verbinden</string> <string name="link_a_mobile">Zu einem Mobiltelefon verbinden</string>
<string name="settings_section_title_use_from_desktop">Vom Desktop aus nutzen</string> <string name="settings_section_title_use_from_desktop">Vom Desktop aus nutzen</string>

View File

@ -1486,7 +1486,7 @@
<string name="desktop_device">Bureau</string> <string name="desktop_device">Bureau</string>
<string name="connected_to_desktop">Connecté au bureau</string> <string name="connected_to_desktop">Connecté au bureau</string>
<string name="this_device_name">Ce nom d\'appareil</string> <string name="this_device_name">Ce nom d\'appareil</string>
<string name="waiting_for_mobile_to_connect_on_port"><![CDATA[En attente d\'une connexion mobile sur le port <i>%s</i>]]></string> <string name="waiting_for_mobile_to_connect">En attente d\'une connexion mobile:</string>
<string name="loading_remote_file_title">Chargement du fichier</string> <string name="loading_remote_file_title">Chargement du fichier</string>
<string name="connecting_to_desktop">Connexion au bureau</string> <string name="connecting_to_desktop">Connexion au bureau</string>
<string name="desktop_devices">Appareils de bureau</string> <string name="desktop_devices">Appareils de bureau</string>

View File

@ -1517,6 +1517,6 @@
<string name="v5_4_more_things_descr">- avvisa facoltativamente i contatti eliminati. <string name="v5_4_more_things_descr">- avvisa facoltativamente i contatti eliminati.
\n- nomi del profilo con spazi. \n- nomi del profilo con spazi.
\n- e molto altro!</string> \n- e molto altro!</string>
<string name="waiting_for_mobile_to_connect_on_port"><![CDATA[In attesa che il cellulare si connetta alla porta <i>%s</i>]]></string> <string name="waiting_for_mobile_to_connect">In attesa che il cellulare si connette:</string>
<string name="group_member_role_author">autore</string> <string name="group_member_role_author">autore</string>
</resources> </resources>

View File

@ -1516,7 +1516,7 @@
\n- en meer!</string> \n- en meer!</string>
<string name="remote_host_was_disconnected_toast"><![CDATA[Mobiele verbinding <b>%s</b> is verbroken]]></string> <string name="remote_host_was_disconnected_toast"><![CDATA[Mobiele verbinding <b>%s</b> is verbroken]]></string>
<string name="group_member_role_author">auteur</string> <string name="group_member_role_author">auteur</string>
<string name="waiting_for_mobile_to_connect_on_port"><![CDATA[Wachten tot mobiel verbinding maakt op poort <i>%s</i>]]></string> <string name="waiting_for_mobile_to_connect">Wachten tot mobiel verbinding maakt:</string>
<string name="multicast_connect_automatically">Automatisch verbinden</string> <string name="multicast_connect_automatically">Automatisch verbinden</string>
<string name="waiting_for_desktop">Wachten op desktop…</string> <string name="waiting_for_desktop">Wachten op desktop…</string>
<string name="found_desktop">Desktop gevonden</string> <string name="found_desktop">Desktop gevonden</string>

View File

@ -1496,7 +1496,7 @@
<string name="v5_4_better_groups_descr">Szybsze dołączenie i bardziej niezawodne wiadomości.</string> <string name="v5_4_better_groups_descr">Szybsze dołączenie i bardziej niezawodne wiadomości.</string>
<string name="linked_mobiles">Połączone telefony</string> <string name="linked_mobiles">Połączone telefony</string>
<string name="this_device_name">Nazwa tego urządzenia</string> <string name="this_device_name">Nazwa tego urządzenia</string>
<string name="waiting_for_mobile_to_connect_on_port"><![CDATA[Oczekiwanie na połączenie telefonu na port <i>%s</i>]]></string> <string name="waiting_for_mobile_to_connect">Oczekiwanie na połączenie telefonu:</string>
<string name="loading_remote_file_title">Ładowanie pliku</string> <string name="loading_remote_file_title">Ładowanie pliku</string>
<string name="found_desktop">Znaleziono komputer</string> <string name="found_desktop">Znaleziono komputer</string>
<string name="desktop_devices">Urządzenia komputerowe</string> <string name="desktop_devices">Urządzenia komputerowe</string>

View File

@ -1601,7 +1601,7 @@
<string name="verify_connection">Проверить соединение</string> <string name="verify_connection">Проверить соединение</string>
<string name="multicast_connect_automatically">Соединяться автоматически</string> <string name="multicast_connect_automatically">Соединяться автоматически</string>
<string name="waiting_for_desktop">Ожидается подключение…</string> <string name="waiting_for_desktop">Ожидается подключение…</string>
<string name="waiting_for_mobile_to_connect_on_port"><![CDATA[Ожидается подключение мобильного через порт <i>%s</i>]]></string> <string name="waiting_for_mobile_to_connect">Ожидается подключение мобильного:</string>
<string name="found_desktop">Компьютер найден</string> <string name="found_desktop">Компьютер найден</string>
<string name="not_compatible">Несовместимая версия!</string> <string name="not_compatible">Несовместимая версия!</string>
<string name="group_member_role_author">автор</string> <string name="group_member_role_author">автор</string>

View File

@ -1517,7 +1517,7 @@
<string name="v5_4_more_things_descr">- 可选择通知已删除的联系人。 <string name="v5_4_more_things_descr">- 可选择通知已删除的联系人。
\n- 带空格的个人资料名称。 \n- 带空格的个人资料名称。
\n- 以及更多!</string> \n- 以及更多!</string>
<string name="waiting_for_mobile_to_connect_on_port"><![CDATA[正等待移动设备在端口 <i>%s</i> 进行连接]]></string> <string name="waiting_for_mobile_to_connect">正等待移动设备 进行连接:</string>
<string name="group_member_role_author">作者</string> <string name="group_member_role_author">作者</string>
<string name="multicast_connect_automatically">自动连接</string> <string name="multicast_connect_automatically">自动连接</string>
<string name="waiting_for_desktop">等待桌面中…</string> <string name="waiting_for_desktop">等待桌面中…</string>

View File

@ -9,30 +9,73 @@ import androidx.compose.material.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.awt.ComposeWindow
import androidx.compose.ui.input.key.* import androidx.compose.ui.input.key.*
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.* import androidx.compose.ui.window.*
import chat.simplex.common.model.ChatController import chat.simplex.common.model.ChatController
import chat.simplex.common.model.ChatModel 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.DEFAULT_START_MODAL_WIDTH
import chat.simplex.common.ui.theme.SimpleXTheme import chat.simplex.common.ui.theme.SimpleXTheme
import chat.simplex.common.views.TerminalView import chat.simplex.common.views.TerminalView
import chat.simplex.common.views.helpers.FileDialogChooser import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.helpers.escapedHtmlToAnnotatedString
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.awt.event.WindowEvent import java.awt.event.WindowEvent
import java.awt.event.WindowFocusListener import java.awt.event.WindowFocusListener
import java.io.File import java.io.File
import kotlin.system.exitProcess
val simplexWindowState = SimplexWindowState() 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<Boolean>) {
// Creates file if not exists; comes with proper defaults // Creates file if not exists; comes with proper defaults
val state = getStoredWindowState() val state = getStoredWindowState()
val windowState: WindowState = rememberWindowState( val windowState: WindowState = rememberWindowState(
placement = WindowPlacement.Floating, placement = WindowPlacement.Floating,
width = state.width.dp, width = state.width.dp,
@ -46,59 +89,62 @@ fun showApp() = application {
windowState.size.width.value, windowState.size.width.value,
windowState.size.height.value windowState.size.height.value
) { ) {
storeWindowState(WindowPositionSize( storeWindowState(
x = windowState.position.x.value.toInt(), WindowPositionSize(
y = windowState.position.y.value.toInt(), x = windowState.position.x.value.toInt(),
width = windowState.size.width.value.toInt(), y = windowState.position.y.value.toInt(),
height = windowState.size.height.value.toInt() width = windowState.size.width.value.toInt(),
)) height = windowState.size.height.value.toInt()
)
)
} }
simplexWindowState.windowState = windowState simplexWindowState.windowState = windowState
// Reload all strings in all @Composable's after language change at runtime // Reload all strings in all @Composable's after language change at runtime
if (remember { ChatController.appPrefs.appLanguage.state }.value != "") { 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) { if (it.key == Key.Escape && it.type == KeyEventType.KeyUp) {
simplexWindowState.backstack.lastOrNull()?.invoke() != null simplexWindowState.backstack.lastOrNull()?.invoke() != null
} else { } else {
false false
} }
}, title = "SimpleX") { }, title = "SimpleX") {
SimpleXTheme { simplexWindowState.window = window
AppScreen() AppScreen()
if (simplexWindowState.openDialog.isAwaiting) { if (simplexWindowState.openDialog.isAwaiting) {
FileDialogChooser( FileDialogChooser(
title = "SimpleX", title = "SimpleX",
isLoad = true, isLoad = true,
params = simplexWindowState.openDialog.params, params = simplexWindowState.openDialog.params,
onResult = { onResult = {
simplexWindowState.openDialog.onResult(it.firstOrNull()) simplexWindowState.openDialog.onResult(it.firstOrNull())
} }
) )
} }
if (simplexWindowState.openMultipleDialog.isAwaiting) { if (simplexWindowState.openMultipleDialog.isAwaiting) {
FileDialogChooser( FileDialogChooser(
title = "SimpleX", title = "SimpleX",
isLoad = true, isLoad = true,
params = simplexWindowState.openMultipleDialog.params, params = simplexWindowState.openMultipleDialog.params,
onResult = { onResult = {
simplexWindowState.openMultipleDialog.onResult(it) simplexWindowState.openMultipleDialog.onResult(it)
} }
) )
} }
if (simplexWindowState.saveDialog.isAwaiting) { if (simplexWindowState.saveDialog.isAwaiting) {
FileDialogChooser( FileDialogChooser(
title = "SimpleX", title = "SimpleX",
isLoad = false, isLoad = false,
params = simplexWindowState.saveDialog.params, params = simplexWindowState.saveDialog.params,
onResult = { simplexWindowState.saveDialog.onResult(it.firstOrNull()) } onResult = { simplexWindowState.saveDialog.onResult(it.firstOrNull()) }
) )
} }
val toasts = remember { simplexWindowState.toasts } val toasts = remember { simplexWindowState.toasts }
val toast = toasts.firstOrNull() val toast = toasts.firstOrNull()
if (toast != null) { if (toast != null) {
SimpleXTheme {
Box(Modifier.fillMaxSize().padding(bottom = 20.dp), contentAlignment = Alignment.BottomCenter) { Box(Modifier.fillMaxSize().padding(bottom = 20.dp), contentAlignment = Alignment.BottomCenter) {
Text( Text(
escapedHtmlToAnnotatedString(toast.first, LocalDensity.current), escapedHtmlToAnnotatedString(toast.first, LocalDensity.current),
@ -107,11 +153,11 @@ fun showApp() = application {
style = MaterialTheme.typography.body1 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) { // Shows toast in insertion order with preferred delay per toast. New one will be shown once previous one expires
delay(toast.second) LaunchedEffect(toast, toasts.size) {
simplexWindowState.toasts.removeFirst() delay(toast.second)
} simplexWindowState.toasts.removeFirst()
} }
} }
var windowFocused by remember { simplexWindowState.windowFocused } var windowFocused by remember { simplexWindowState.windowFocused }
@ -160,6 +206,7 @@ class SimplexWindowState {
val saveDialog = DialogState<File?>() val saveDialog = DialogState<File?>()
val toasts = mutableStateListOf<Pair<String, Long>>() val toasts = mutableStateListOf<Pair<String, Long>>()
var windowFocused = mutableStateOf(true) var windowFocused = mutableStateOf(true)
var window: ComposeWindow? = null
} }
data class DialogParams( data class DialogParams(
@ -188,7 +235,5 @@ class DialogState<T> {
@Preview @Preview
@Composable @Composable
fun AppPreview() { fun AppPreview() {
SimpleXTheme { AppScreen()
AppScreen()
}
} }

View File

@ -19,3 +19,9 @@ actual fun getKeyboardState(): State<KeyboardState> = remember { mutableStateOf(
actual fun hideKeyboard(view: Any?) {} actual fun hideKeyboard(view: Any?) {}
actual fun androidIsFinishingMainActivity(): Boolean = false 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())
}
}

View File

@ -6,6 +6,7 @@ import androidx.compose.runtime.Composable
import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.SharedPreference import chat.simplex.common.model.SharedPreference
import chat.simplex.common.model.User import chat.simplex.common.model.User
import chat.simplex.common.platform.chatModel
import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.ui.theme.DEFAULT_PADDING
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource 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<OnboardingStage>, onclick: (() -> Unit)?) { actual fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference<OnboardingStage>, onclick: (() -> Unit)?) {
if (user == null) { if (user == null) {
Row(horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING * 2.5f)) { 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) OnboardingActionButton(MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, true, icon = painterResource(MR.images.ic_desktop), onclick = onclick)
} }
} else { } else {

View File

@ -3,10 +3,6 @@ package chat.simplex.desktop
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.showApp import chat.simplex.common.showApp
import java.io.File 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() { fun main() {
initHaskell() initHaskell()

View File

@ -11,7 +11,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package source-repository-package
type: git type: git
location: https://github.com/simplex-chat/simplexmq.git location: https://github.com/simplex-chat/simplexmq.git
tag: eaf5317834b069144b5f4897f9c79831983e54dd tag: a860936072172e261480fa6bdd95203976e366b2
source-repository-package source-repository-package
type: git type: git

View File

@ -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! 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. 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.

View File

@ -1,5 +1,5 @@
name: simplex-chat name: simplex-chat
version: 5.4.0.6 version: 5.4.0.7
#synopsis: #synopsis:
#description: #description:
homepage: https://github.com/simplex-chat/simplex-chat#readme homepage: https://github.com/simplex-chat/simplex-chat#readme

View File

@ -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/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/kazu-yamamoto/http2.git"."f5525b755ff2418e6e6ecc69e877363b0d0bcaeb" = "0fyx0047gvhm99ilp212mmz37j84cwrfnpmssib5dw363fyb88b6"; "https://github.com/kazu-yamamoto/http2.git"."f5525b755ff2418e6e6ecc69e877363b0d0bcaeb" = "0fyx0047gvhm99ilp212mmz37j84cwrfnpmssib5dw363fyb88b6";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "0kiwhvml42g9anw4d2v0zd1fpc790pj9syg5x3ik4l97fnkbbwpp"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "0kiwhvml42g9anw4d2v0zd1fpc790pj9syg5x3ik4l97fnkbbwpp";

View File

@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack -- see: https://github.com/sol/hpack
name: simplex-chat name: simplex-chat
version: 5.4.0.6 version: 5.4.0.7
category: Web, System, Services, Cryptography category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat author: simplex.chat

View File

@ -189,6 +189,7 @@ mobileChatOpts dbFilePrefix dbKey =
allowInstantFiles = True, allowInstantFiles = True,
autoAcceptFileSize = 0, autoAcceptFileSize = 0,
muteNotifications = True, muteNotifications = True,
markRead = False,
maintenance = True maintenance = True
} }