From ce9218b186f1d53ee8dff66c5b3cef53309ef76c Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Mon, 18 Dec 2023 22:04:49 +0400
Subject: [PATCH 01/32] ios: rework authentication (#3556)
---
apps/ios/Shared/ContentView.swift | 157 +++++++++++-------
apps/ios/Shared/Model/ChatModel.swift | 2 +
apps/ios/Shared/SimpleXApp.swift | 52 ++----
.../Views/UserSettings/PrivacySettings.swift | 2 +
4 files changed, 118 insertions(+), 95 deletions(-)
diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift
index b69ccbb7c7..d7b9fef218 100644
--- a/apps/ios/Shared/ContentView.swift
+++ b/apps/ios/Shared/ContentView.swift
@@ -14,11 +14,14 @@ struct ContentView: View {
@ObservedObject var alertManager = AlertManager.shared
@ObservedObject var callController = CallController.shared
@Environment(\.colorScheme) var colorScheme
- @Binding var doAuthenticate: Bool
- @Binding var userAuthorized: Bool?
- @Binding var canConnectCall: Bool
- @Binding var lastSuccessfulUnlock: TimeInterval?
- @Binding var showInitializationView: Bool
+
+ var contentAccessAuthenticationExtended: Bool
+
+ @Environment(\.scenePhase) var scenePhase
+ @State private var automaticAuthenticationAttempted = false
+ @State private var canConnectViewCall = false
+ @State private var lastSuccessfulUnlock: TimeInterval? = nil
+
@AppStorage(DEFAULT_SHOW_LA_NOTICE) private var prefShowLANotice = false
@AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@@ -40,9 +43,19 @@ struct ContentView: View {
}
}
+ private var accessAuthenticated: Bool {
+ chatModel.contentViewAccessAuthenticated || contentAccessAuthenticationExtended
+ }
+
var body: some View {
ZStack {
- contentView()
+ // contentView() has to be in a single branch, so that enabling authentication doesn't trigger re-rendering and close settings.
+ // i.e. with separate branches like this settings are closed: `if prefPerformLA { ... contentView() ... } else { contentView() }
+ if !prefPerformLA || accessAuthenticated {
+ contentView()
+ } else {
+ lockButton()
+ }
if chatModel.showCallView, let call = chatModel.activeCall {
callView(call)
}
@@ -50,6 +63,7 @@ struct ContentView: View {
LocalAuthView(authRequest: la)
} else if showSetPasscode {
SetAppPasscodeView {
+ chatModel.contentViewAccessAuthenticated = true
prefPerformLA = true
showSetPasscode = false
privacyLocalAuthModeDefault.set(.passcode)
@@ -60,13 +74,9 @@ struct ContentView: View {
alertManager.showAlert(laPasscodeNotSetAlert())
}
}
- }
- .onAppear {
- if prefPerformLA { requestNtfAuthorization() }
- initAuthenticate()
- }
- .onChange(of: doAuthenticate) { _ in
- initAuthenticate()
+ if chatModel.chatDbStatus == nil {
+ initializationView()
+ }
}
.alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
.sheet(isPresented: $showSettings) {
@@ -76,14 +86,44 @@ struct ContentView: View {
Button("System authentication") { initialEnableLA() }
Button("Passcode entry") { showSetPasscode = true }
}
+ .onChange(of: scenePhase) { phase in
+ logger.debug("scenePhase was \(String(describing: scenePhase)), now \(String(describing: phase))")
+ switch (phase) {
+ case .background:
+ // also see .onChange(of: scenePhase) in SimpleXApp: on entering background
+ // it remembers enteredBackgroundAuthenticated and sets chatModel.contentViewAccessAuthenticated to false
+ automaticAuthenticationAttempted = false
+ canConnectViewCall = false
+ case .active:
+ canConnectViewCall = !prefPerformLA || contentAccessAuthenticationExtended || unlockedRecently()
+
+ // condition `!chatModel.contentViewAccessAuthenticated` is required for when authentication is enabled in settings or on initial notice
+ if prefPerformLA && !chatModel.contentViewAccessAuthenticated {
+ if AppChatState.shared.value != .stopped {
+ if contentAccessAuthenticationExtended {
+ chatModel.contentViewAccessAuthenticated = true
+ } else {
+ if !automaticAuthenticationAttempted {
+ automaticAuthenticationAttempted = true
+ // authenticate if call kit call is not in progress
+ if !(CallController.useCallKit() && chatModel.showCallView && chatModel.activeCall != nil) {
+ authenticateContentViewAccess()
+ }
+ }
+ }
+ } else {
+ // when app is stopped automatic authentication is not attempted
+ chatModel.contentViewAccessAuthenticated = contentAccessAuthenticationExtended
+ }
+ }
+ default:
+ break
+ }
+ }
}
@ViewBuilder private func contentView() -> some View {
- if prefPerformLA && userAuthorized != true {
- lockButton()
- } else if chatModel.chatDbStatus == nil && showInitializationView {
- initializationView()
- } else if let status = chatModel.chatDbStatus, status != .ok {
+ if let status = chatModel.chatDbStatus, status != .ok {
DatabaseErrorView(status: status)
} else if !chatModel.v3DBMigration.startChat {
MigrateToAppGroupView()
@@ -106,11 +146,11 @@ struct ContentView: View {
if CallController.useCallKit() {
ActiveCallView(call: call, canConnectCall: Binding.constant(true))
.onDisappear {
- if userAuthorized == false && doAuthenticate { runAuthenticate() }
+ if prefPerformLA && !accessAuthenticated { authenticateContentViewAccess() }
}
} else {
- ActiveCallView(call: call, canConnectCall: $canConnectCall)
- if prefPerformLA && userAuthorized != true {
+ ActiveCallView(call: call, canConnectCall: $canConnectViewCall)
+ if prefPerformLA && !accessAuthenticated {
Rectangle()
.fill(colorScheme == .dark ? .black : .white)
.frame(maxWidth: .infinity, maxHeight: .infinity)
@@ -120,22 +160,27 @@ struct ContentView: View {
}
private func lockButton() -> some View {
- Button(action: runAuthenticate) { Label("Unlock", systemImage: "lock") }
+ Button(action: authenticateContentViewAccess) { Label("Unlock", systemImage: "lock") }
}
private func initializationView() -> some View {
VStack {
ProgressView().scaleEffect(2)
- Text("Opening database…")
+ Text("Opening app…")
.padding()
}
+ .frame(maxWidth: .infinity, maxHeight: .infinity )
+ .background(
+ Rectangle()
+ .fill(.background)
+ )
}
private func mainView() -> some View {
ZStack(alignment: .top) {
ChatListView(showSettings: $showSettings).privacySensitive(protectScreen)
.onAppear {
- if !prefPerformLA { requestNtfAuthorization() }
+ requestNtfAuthorization()
// Local Authentication notice is to be shown on next start after onboarding is complete
if (!prefLANoticeShown && prefShowLANotice && !chatModel.chats.isEmpty) {
prefLANoticeShown = true
@@ -187,48 +232,37 @@ struct ContentView: View {
}
}
- private func initAuthenticate() {
- logger.debug("initAuthenticate")
- if CallController.useCallKit() && chatModel.showCallView && chatModel.activeCall != nil {
- userAuthorized = false
- } else if doAuthenticate {
- runAuthenticate()
- }
- }
-
- private func runAuthenticate() {
- logger.debug("DEBUGGING: runAuthenticate")
- if !prefPerformLA {
- userAuthorized = true
+ private func unlockedRecently() -> Bool {
+ if let lastSuccessfulUnlock = lastSuccessfulUnlock {
+ return ProcessInfo.processInfo.systemUptime - lastSuccessfulUnlock < 2
} else {
- logger.debug("DEBUGGING: before dismissAllSheets")
- dismissAllSheets(animated: false) {
- logger.debug("DEBUGGING: in dismissAllSheets callback")
- chatModel.chatId = nil
- justAuthenticate()
- }
+ return false
}
}
- private func justAuthenticate() {
- userAuthorized = false
- let laMode = privacyLocalAuthModeDefault.get()
- authenticate(reason: NSLocalizedString("Unlock app", comment: "authentication reason"), selfDestruct: true) { laResult in
- logger.debug("DEBUGGING: authenticate callback: \(String(describing: laResult))")
- switch (laResult) {
- case .success:
- userAuthorized = true
- canConnectCall = true
- lastSuccessfulUnlock = ProcessInfo.processInfo.systemUptime
- case .failed:
- if laMode == .passcode {
- AlertManager.shared.showAlert(laFailedAlert())
+ private func authenticateContentViewAccess() {
+ logger.debug("DEBUGGING: authenticateContentViewAccess")
+ dismissAllSheets(animated: false) {
+ logger.debug("DEBUGGING: authenticateContentViewAccess, in dismissAllSheets callback")
+ chatModel.chatId = nil
+
+ authenticate(reason: NSLocalizedString("Unlock app", comment: "authentication reason"), selfDestruct: true) { laResult in
+ logger.debug("DEBUGGING: authenticate callback: \(String(describing: laResult))")
+ switch (laResult) {
+ case .success:
+ chatModel.contentViewAccessAuthenticated = true
+ canConnectViewCall = true
+ lastSuccessfulUnlock = ProcessInfo.processInfo.systemUptime
+ case .failed:
+ chatModel.contentViewAccessAuthenticated = false
+ if privacyLocalAuthModeDefault.get() == .passcode {
+ AlertManager.shared.showAlert(laFailedAlert())
+ }
+ case .unavailable:
+ prefPerformLA = false
+ canConnectViewCall = true
+ AlertManager.shared.showAlert(laUnavailableTurningOffAlert())
}
- case .unavailable:
- userAuthorized = true
- prefPerformLA = false
- canConnectCall = true
- AlertManager.shared.showAlert(laUnavailableTurningOffAlert())
}
}
}
@@ -259,6 +293,7 @@ struct ContentView: View {
authenticate(reason: NSLocalizedString("Enable SimpleX Lock", comment: "authentication reason")) { laResult in
switch laResult {
case .success:
+ chatModel.contentViewAccessAuthenticated = true
prefPerformLA = true
alertManager.showAlert(laTurnedOnAlert())
case .failed:
diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift
index e7932f2d92..0cc281fda9 100644
--- a/apps/ios/Shared/Model/ChatModel.swift
+++ b/apps/ios/Shared/Model/ChatModel.swift
@@ -54,6 +54,8 @@ final class ChatModel: ObservableObject {
@Published var chatDbChanged = false
@Published var chatDbEncrypted: Bool?
@Published var chatDbStatus: DBMigrationResult?
+ // local authentication
+ @Published var contentViewAccessAuthenticated: Bool = false
@Published var laRequest: LocalAuthRequest?
// list of chat "previews"
@Published var chats: [Chat] = []
diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift
index 057188c37c..c023f375d3 100644
--- a/apps/ios/Shared/SimpleXApp.swift
+++ b/apps/ios/Shared/SimpleXApp.swift
@@ -16,18 +16,13 @@ struct SimpleXApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject private var chatModel = ChatModel.shared
@ObservedObject var alertManager = AlertManager.shared
+
@Environment(\.scenePhase) var scenePhase
- @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
- @State private var userAuthorized: Bool?
- @State private var doAuthenticate = false
- @State private var enteredBackground: TimeInterval? = nil
- @State private var canConnectCall = false
- @State private var lastSuccessfulUnlock: TimeInterval? = nil
- @State private var showInitializationView = false
+ @State private var enteredBackgroundAuthenticated: TimeInterval? = nil
init() {
// DispatchQueue.global(qos: .background).sync {
- haskell_init()
+ haskell_init()
// hs_init(0, nil)
// }
UserDefaults.standard.register(defaults: appDefaults)
@@ -39,21 +34,16 @@ struct SimpleXApp: App {
}
var body: some Scene {
- return WindowGroup {
- ContentView(
- doAuthenticate: $doAuthenticate,
- userAuthorized: $userAuthorized,
- canConnectCall: $canConnectCall,
- lastSuccessfulUnlock: $lastSuccessfulUnlock,
- showInitializationView: $showInitializationView
- )
+ WindowGroup {
+ // contentAccessAuthenticationExtended has to be passed to ContentView on view initialization,
+ // so that it's computed by the time view renders, and not on event after rendering
+ ContentView(contentAccessAuthenticationExtended: !authenticationExpired())
.environmentObject(chatModel)
.onOpenURL { url in
logger.debug("ContentView.onOpenURL: \(url)")
chatModel.appOpenUrl = url
}
.onAppear() {
- showInitializationView = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
initChatAndMigrate()
}
@@ -62,21 +52,25 @@ struct SimpleXApp: App {
logger.debug("scenePhase was \(String(describing: scenePhase)), now \(String(describing: phase))")
switch (phase) {
case .background:
+ // --- authentication
+ // see ContentView .onChange(of: scenePhase) for remaining authentication logic
+ if chatModel.contentViewAccessAuthenticated {
+ enteredBackgroundAuthenticated = ProcessInfo.processInfo.systemUptime
+ }
+ chatModel.contentViewAccessAuthenticated = false
+ // authentication ---
+
if CallController.useCallKit() && chatModel.activeCall != nil {
CallController.shared.shouldSuspendChat = true
} else {
suspendChat()
BGManager.shared.schedule()
}
- if userAuthorized == true {
- enteredBackground = ProcessInfo.processInfo.systemUptime
- }
- doAuthenticate = false
- canConnectCall = false
NtfManager.shared.setNtfBadgeCount(chatModel.totalUnreadCountForAllUsers())
case .active:
CallController.shared.shouldSuspendChat = false
let appState = AppChatState.shared.value
+
if appState != .stopped {
startChatAndActivate {
if appState.inactive && chatModel.chatRunning == true {
@@ -85,8 +79,6 @@ struct SimpleXApp: App {
updateCallInvitations()
}
}
- doAuthenticate = authenticationExpired()
- canConnectCall = !(doAuthenticate && prefPerformLA) || unlockedRecently()
}
}
default:
@@ -121,22 +113,14 @@ struct SimpleXApp: App {
}
private func authenticationExpired() -> Bool {
- if let enteredBackground = enteredBackground {
+ if let enteredBackgroundAuthenticated = enteredBackgroundAuthenticated {
let delay = Double(UserDefaults.standard.integer(forKey: DEFAULT_LA_LOCK_DELAY))
- return ProcessInfo.processInfo.systemUptime - enteredBackground >= delay
+ return ProcessInfo.processInfo.systemUptime - enteredBackgroundAuthenticated >= delay
} else {
return true
}
}
- private func unlockedRecently() -> Bool {
- if let lastSuccessfulUnlock = lastSuccessfulUnlock {
- return ProcessInfo.processInfo.systemUptime - lastSuccessfulUnlock < 2
- } else {
- return false
- }
- }
-
private func updateChats() {
do {
let chats = try apiGetChats()
diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
index 90b83fa4f3..d8ff2c2f89 100644
--- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
+++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
@@ -467,6 +467,7 @@ struct SimplexLockView: View {
switch a {
case .enableAuth:
SetAppPasscodeView {
+ m.contentViewAccessAuthenticated = true
laLockDelay = 30
prefPerformLA = true
showChangePassword = true
@@ -619,6 +620,7 @@ struct SimplexLockView: View {
authenticate(reason: NSLocalizedString("Enable SimpleX Lock", comment: "authentication reason")) { laResult in
switch laResult {
case .success:
+ m.contentViewAccessAuthenticated = true
prefPerformLA = true
laAlert = .laTurnedOnAlert
case .failed:
From 26a189917bb7a93b704dda09be2298d7c6e593e3 Mon Sep 17 00:00:00 2001
From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
Date: Tue, 19 Dec 2023 05:37:10 +0800
Subject: [PATCH 02/32] sctipt: check string formatting (#3570)
* sctipt: check string formatting
* all
---
apps/multiplatform/common/build.gradle.kts | 64 +++++++++++++++++--
.../commonMain/resources/MR/ar/strings.xml | 1 -
.../commonMain/resources/MR/el/strings.xml | 2 +-
.../commonMain/resources/MR/es/strings.xml | 2 +-
.../commonMain/resources/MR/fi/strings.xml | 1 -
.../commonMain/resources/MR/ja/strings.xml | 2 -
.../commonMain/resources/MR/tr/strings.xml | 6 +-
.../resources/MR/zh-rCN/strings.xml | 3 +-
.../resources/MR/zh-rTW/strings.xml | 2 +-
9 files changed, 67 insertions(+), 16 deletions(-)
diff --git a/apps/multiplatform/common/build.gradle.kts b/apps/multiplatform/common/build.gradle.kts
index 4b0e38d8a0..32bfadd37e 100644
--- a/apps/multiplatform/common/build.gradle.kts
+++ b/apps/multiplatform/common/build.gradle.kts
@@ -155,6 +155,34 @@ afterEvaluate {
val endTagRegex = Regex("")
val anyHtmlRegex = Regex("[^>]*>.*(<|>).*|[^>]*>.*(<|>).*")
val correctHtmlRegex = Regex("[^>]*>.*.*.*|[^>]*>.*.*.*|[^>]*>.*.*.*|[^>]*>.*]*>.*.*")
+ val possibleFormat = listOf("s", "d", "1\$s", "1\$d", "2s", "f")
+
+ fun String.id(): String = replace(" {
+ if (!contains("%")) return emptyList()
+ val value = substringAfter("\">").substringBeforeLast("")
+
+ val formats = ArrayList()
+ var substring = value.substringAfter("%")
+ while (true) {
+ var foundFormat = false
+ for (format in possibleFormat) {
+ if (substring.startsWith(format)) {
+ formats.add(format)
+ foundFormat = true
+ break
+ }
+ }
+ if (!foundFormat) {
+ throw Exception("Unknown formatting in string. Add it to 'possibleFormat' in common/build.gradle.kts if needed: $this \nin $filepath")
+ }
+ val was = substring
+ substring = substring.substringAfter("%")
+ if (was.length == substring.length) break
+ }
+ return formats
+ }
fun String.removeCDATA(): String =
if (contains("
+ val tree = kotlin.sourceSets["commonMain"].resources.filter { fileRegex.containsMatchIn(it.absolutePath) }.asFileTree
+ val baseStringsFile = tree.first { it.absolutePath.endsWith("base/strings.xml") } ?: throw Exception("No base/strings.xml found")
+ val treeList = ArrayList(tree.toList())
+ treeList.remove(baseStringsFile)
+ treeList.add(0, baseStringsFile)
+ val baseFormatting = mutableMapOf>()
+ treeList.forEachIndexed { index, file ->
+ val isBase = index == 0
val initialLines = ArrayList()
val finalLines = ArrayList()
+ val errors = ArrayList()
+
file.useLines { lines ->
val multiline = ArrayList()
lines.forEach { line ->
initialLines.add(line)
if (stringRegex.matches(line)) {
- finalLines.add(line.removeCDATA().addCDATA(file.absolutePath))
+ val fixedLine = line.removeCDATA().addCDATA(file.absolutePath)
+ val lineId = fixedLine.id()
+ if (isBase) {
+ baseFormatting[lineId] = fixedLine.formatting(file.absolutePath)
+ } else if (baseFormatting[lineId] != fixedLine.formatting(file.absolutePath)) {
+ errors.add("Incorrect formatting in string: $fixedLine \nin ${file.absolutePath}")
+ }
+ finalLines.add(fixedLine)
} else if (multiline.isEmpty() && startStringRegex.containsMatchIn(line)) {
multiline.add(line)
} else if (multiline.isNotEmpty() && endStringRegex.containsMatchIn(line)) {
multiline.add(line)
- finalLines.addAll(multiline.joinToString("\n").removeCDATA().addCDATA(file.absolutePath).split("\n"))
+ val fixedLines = multiline.joinToString("\n").removeCDATA().addCDATA(file.absolutePath).split("\n")
+ val fixedLinesJoined = fixedLines.joinToString("")
+ val lineId = fixedLinesJoined.id()
+ if (isBase) {
+ baseFormatting[lineId] = fixedLinesJoined.formatting(file.absolutePath)
+ } else if (baseFormatting[lineId] != fixedLinesJoined.formatting(file.absolutePath)) {
+ errors.add("Incorrect formatting in string: $fixedLinesJoined \nin ${file.absolutePath}")
+ }
+ finalLines.addAll(fixedLines)
multiline.clear()
} else if (multiline.isNotEmpty()) {
multiline.add(line)
@@ -217,10 +269,14 @@ afterEvaluate {
}
}
if (multiline.isNotEmpty()) {
- throw Exception("Unclosed string tag: ${multiline.joinToString("\n")} \nin ${file.absolutePath}")
+ errors.add("Unclosed string tag: ${multiline.joinToString("\n")} \nin ${file.absolutePath}")
}
}
+ if (errors.isNotEmpty()) {
+ throw Exception("Found errors: \n\n${errors.joinToString("\n\n")}")
+ }
+
if (!debug && finalLines != initialLines) {
file.writer().use {
finalLines.forEachIndexed { index, line ->
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml
index 098c748355..fd5a827ba2 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml
@@ -150,7 +150,6 @@
إضافة جهة اتصال جديدة : لإنشاء رمز الاستجابة السريعة الخاص بك لمرة واحدة لجهة اتصالك.]]>
امسح رمز الاستجابة السريعة : للاتصال بجهة الاتصال التي تعرض لك رمز الاستجابة السريعة.]]>
مكالمتك تحت الإجراء
- انتهت المكالمة
تغيير عبارة مرور قاعدة البيانات؟
لا يمكن الوصول إلى Keystore لحفظ كلمة مرور قاعدة البيانات
إلغاء معاينة الملف
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml
index 714f31732c..7063eb9007 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml
@@ -41,7 +41,7 @@
Αποδοχή
Αποδοχή αιτήματος σύνδεσης;
αποδεκτή κλήση
- Πρόσβαση στους διακομιστές μέσω SOCKS proxy στην πόρτα 9050; Ο διακομιστής μεσολάβησης (proxy server) πρέπει να είναι ενεργός πριν ενεργοποιηθεί αυτή η ρύθμιση.
+ Πρόσβαση στους διακομιστές μέσω SOCKS proxy στην πόρτα %d; Ο διακομιστής μεσολάβησης (proxy server) πρέπει να είναι ενεργός πριν ενεργοποιηθεί αυτή η ρύθμιση.
Προσθήκη διακομιστή…
Προχωρημένες ρυθμίσεις δικτύου
Προσθήκη διακομιστών μέσω σάρωσης QR κωδικών.
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml
index 08cc7f9820..381d28afa6 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml
@@ -298,7 +298,7 @@
Cancelar mensaje en directo
Confirmar
Vaciar
- Build de la aplicación
+ Build de la aplicación: %s
¡La llamada ha terminado!
el servidor de envío ha cambiado para tí
cancelar vista previa del enlace
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml
index ce8692130f..5410778c44 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml
@@ -171,7 +171,6 @@
Arkisto
Poista keskusteluarkisto\?
Luotu %1$s
- %s:n rooli muutettu %s:ksi
poistettu ryhmä
yhdistää
yhdistäminen (hyväksytty)
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml
index 94a35dbd8b..fb03dd1f28 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml
@@ -687,7 +687,6 @@
ファイル送信が中止されました。
送信元が繋がりリクエストを削除したかもしれません。
このサーバで待ち行列を作るには認証が必要です。パスワードをご確認ください。
- アプリが定期的に新しいメッセージを受信します。一日の電池使用量が約3%で、プッシュ通知に頼らずに、あなたの端末のデータをサーバに送ることはありません。
SimpleXロック
通知を受けるには、データベースの暗証フレーズを入力してください。
SimpleX Chat サービス
@@ -904,7 +903,6 @@
SIMPLEX CHATを支援
テストサーバ
受信アドレスは別のサーバーに変更されます。アドレス変更は送信者がオンラインになった後に完了します。
- SimpleX バックグラウンド・サービス を使ってます。一日の電池使用量は約3%です。]]>
あなたのプライバシーを守るために、他のアプリと違って、ユーザーIDの変わりに SimpleX メッセージ束毎にIDを配布し、各連絡先が別々と扱います。
あなたのチャットプロフィールが他のグループメンバーに送られます。
エンドツーエンド暗号化を確認するには、ご自分の端末と連絡先の端末のコードを比べます (スキャンします)。
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml
index 9824b0d8f0..d7df9655d3 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml
@@ -337,7 +337,7 @@
Mesaj gönderilirken hata oluştu
Adres oluştururken hata oluştu
Adres değiştirirken hata oluştu
- 1$s sizinle şu yolla bağlantı kurmak istiyor
+ %1$s sizinle şu yolla bağlantı kurmak istiyor
Ayarları değiştirirken hata oluştu
Toplu konuşma bağlantısı oluştururken hata oluştu
Yetki değiştirirken hata oluştu
@@ -747,9 +747,9 @@
Aklınızda bulunsun: kaybederseniz, parolayı kurtaramaz veya değiştiremezsiniz.]]>
Sohbet arşivi
SOHBET ARŞİVİ
- 1$s grubuna davet
+ %1$s grubuna davet
Gruba katıl\?
- 1$s davet edildi
+ %1$s davet edildi
grup bağlantınız üzerinden davet edildi
davet edildi
Gruba davet edin
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 ed2b9986c0..31be2f187c 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
@@ -1344,7 +1344,7 @@
我们错过的第二个\"√\"!✅
设定数据库密码
为群组禁用回执吗?
- %s、%s 和 %d 已连接
+ %s、%s 和 %s 已连接
修复群组成员不支持的问题
已为 %d 组启用送达回执功能
重新协商
@@ -1427,7 +1427,6 @@
通过链接进行连接吗?
已经加入了该群组!
%s、 %s 和 %d 名成员
- %s 审核了 %d 条消息
解封成员
连接到你自己?
轻按连接
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml
index b1d988e465..9caf45dcc1 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml
@@ -11,7 +11,7 @@
關於 SimpleX Chat
接受連接請求?
已接受通話
- 要在端口啟用 SOCKS 代理伺服器嗎?在啟用這個選項之前,必須先啟用代理伺服器。
+ 要在端口啟用 SOCKS 代理伺服器嗎 %d?在啟用這個選項之前,必須先啟用代理伺服器。
管理員
然後,選按:
新增預設伺服器
From 5e042d222eeb54b570946499f2381d142d9f3672 Mon Sep 17 00:00:00 2001
From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
Date: Tue, 19 Dec 2023 18:24:13 +0800
Subject: [PATCH 03/32] desktop: saving qr code as an image (#3572)
---
.../chat/simplex/common/views/newchat/QRCode.kt | 2 +-
.../simplex/common/views/helpers/Utils.desktop.kt | 11 +++++------
2 files changed, 6 insertions(+), 7 deletions(-)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt
index 763addae66..7f9fae60a3 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt
@@ -67,7 +67,7 @@ fun QRCode(
scope.launch {
val image = qrCodeBitmap(connReq, 1024).replaceColor(Color.Black.toArgb(), tintColor.toArgb())
.let { if (withLogo) it.addLogo() else it }
- val file = saveTempImageUncompressed(image, false)
+ val file = saveTempImageUncompressed(image, true)
if (file != null) {
shareFile("", CryptoFile.plain(file.absolutePath))
}
diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt
index eb1792474a..19c9fc0fd7 100644
--- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt
+++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt
@@ -5,11 +5,11 @@ import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Density
-import chat.simplex.common.model.*
+import chat.simplex.common.model.CIFile
+import chat.simplex.common.model.readCryptoFile
import chat.simplex.common.platform.*
import chat.simplex.common.simplexWindowState
-import java.io.ByteArrayInputStream
-import java.io.File
+import java.io.*
import java.net.URI
import javax.imageio.ImageIO
import kotlin.io.encoding.Base64
@@ -148,9 +148,8 @@ actual suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean)
return if (file != null) {
try {
val ext = if (asPng) "png" else "jpg"
- val newFile = File(file.absolutePath + File.separator + generateNewFileName("IMG", ext, File(getAppFilePath(""))))
- // LALAL FILE IS EMPTY
- ImageIO.write(image.toAwtImage(), ext.uppercase(), newFile.outputStream())
+ val newFile = File(file.absolutePath + File.separator + generateNewFileName("IMG", ext, File(file.absolutePath)))
+ ImageIO.write(image.toAwtImage(), ext, newFile.outputStream())
newFile
} catch (e: Exception) {
Log.e(TAG, "Util.kt saveTempImageUncompressed error: ${e.message}")
From 7b073ba9f83e808bc19cff24bbe2732dfd630bd9 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Tue, 19 Dec 2023 10:26:01 +0000
Subject: [PATCH 04/32] core: allow deleting last user (#3567)
* core: allow deleting last user (tests fail)
* tests, allow activating the hidden user when there is no active user
* hide logs
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
* comment
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
* comment
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
---------
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
---
src/Simplex/Chat.hs | 25 +++++++++------
src/Simplex/Chat/View.hs | 4 ++-
tests/ChatClient.hs | 4 +--
tests/ChatTests/Direct.hs | 67 ++++++++++++++++++++++++---------------
4 files changed, 61 insertions(+), 39 deletions(-)
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index dbccfbdfcf..6b619c5bd6 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -473,12 +473,14 @@ processChatCommand = \case
coupleDaysAgo t = (`addUTCTime` t) . fromInteger . negate . (+ (2 * day)) <$> randomRIO (0, day)
day = 86400
ListUsers -> CRUsersList <$> withStoreCtx' (Just "ListUsers, getUsersInfo") getUsersInfo
- APISetActiveUser userId' viewPwd_ -> withUser $ \user -> do
+ APISetActiveUser userId' viewPwd_ -> do
+ unlessM chatStarted $ throwChatError CEChatNotStarted
+ user_ <- chatReadVar currentUser
user' <- privateGetUser userId'
- validateUserPassword user user' viewPwd_
+ validateUserPassword_ user_ user' viewPwd_
withStoreCtx' (Just "APISetActiveUser, setActiveUser") $ \db -> setActiveUser db userId'
let user'' = user' {activeUser = True}
- asks currentUser >>= atomically . (`writeTVar` Just user'')
+ chatWriteVar currentUser $ Just user''
pure $ CRActiveUser user''
SetActiveUser uName viewPwd_ -> do
tryChatError (withStore (`getUserIdByName` uName)) >>= \case
@@ -2300,11 +2302,14 @@ processChatCommand = \case
tryChatError (withStore (`getUser` userId)) >>= \case
Left _ -> throwChatError CEUserUnknown
Right user -> pure user
- validateUserPassword :: User -> User -> Maybe UserPwd -> m ()
- validateUserPassword User {userId} User {userId = userId', viewPwdHash} viewPwd_ =
+ validateUserPassword :: User -> User -> Maybe UserPwd -> m ()
+ validateUserPassword = validateUserPassword_ . Just
+ validateUserPassword_ :: Maybe User -> User -> Maybe UserPwd -> m ()
+ validateUserPassword_ user_ User {userId = userId', viewPwdHash} viewPwd_ =
forM_ viewPwdHash $ \pwdHash ->
- let pwdOk = case viewPwd_ of
- Nothing -> userId == userId'
+ let userId_ = (\User {userId} -> userId) <$> user_
+ pwdOk = case viewPwd_ of
+ Nothing -> userId_ == Just userId'
Just (UserPwd viewPwd) -> validPassword viewPwd pwdHash
in unless pwdOk $ throwChatError CEUserUnknown
validPassword :: Text -> UserPwdHash -> Bool
@@ -2327,16 +2332,16 @@ processChatCommand = \case
pure $ CRUserPrivacy {user, updatedUser = user'}
checkDeleteChatUser :: User -> m ()
checkDeleteChatUser user@User {userId} = do
- when (activeUser user) $ throwChatError (CECantDeleteActiveUser userId)
users <- withStore' getUsers
- unless (length users > 1 && (isJust (viewPwdHash user) || length (filter (isNothing . viewPwdHash) users) > 1)) $
- throwChatError (CECantDeleteLastUser userId)
+ let otherVisible = filter (\User {userId = userId', viewPwdHash} -> userId /= userId' && isNothing viewPwdHash) users
+ when (activeUser user && length otherVisible > 0) $ throwChatError (CECantDeleteActiveUser userId)
deleteChatUser :: User -> Bool -> m ChatResponse
deleteChatUser user delSMPQueues = do
filesInfo <- withStore' (`getUserFileInfo` user)
forM_ filesInfo $ \fileInfo -> deleteFile user fileInfo
withAgent $ \a -> deleteUser a (aUserId user) delSMPQueues
withStore' (`deleteUserRecord` user)
+ when (activeUser user) $ chatWriteVar currentUser Nothing
ok_
updateChatSettings :: ChatName -> (ChatSettings -> ChatSettings) -> m ChatResponse
updateChatSettings (ChatName cType name) updateSettings = withUser $ \user -> do
diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs
index 4f7c8698bf..b0408690ae 100644
--- a/src/Simplex/Chat/View.hs
+++ b/src/Simplex/Chat/View.hs
@@ -474,7 +474,9 @@ chatItemDeletedText ChatItem {meta = CIMeta {itemDeleted}, content} membership_
_ -> ""
viewUsersList :: [UserInfo] -> [StyledString]
-viewUsersList = mapMaybe userInfo . sortOn ldn
+viewUsersList us =
+ let ss = mapMaybe userInfo $ sortOn ldn us
+ in if null ss then ["no users"] else ss
where
ldn (UserInfo User {localDisplayName = n} _) = T.toLower n
userInfo (UserInfo User {localDisplayName = n, profile = LocalProfile {fullName}, activeUser, showNtfs, viewPwdHash} count)
diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs
index 74e29cb0b1..821d7b032b 100644
--- a/tests/ChatClient.hs
+++ b/tests/ChatClient.hs
@@ -18,7 +18,7 @@ import Control.Monad.Except
import Data.ByteArray (ScrubbedBytes)
import Data.Functor (($>))
import Data.List (dropWhileEnd, find)
-import Data.Maybe (fromJust, isNothing)
+import Data.Maybe (isNothing)
import qualified Data.Text as T
import Network.Socket
import Simplex.Chat
@@ -284,7 +284,7 @@ getTermLine cc =
_ -> error "no output for 5 seconds"
userName :: TestCC -> IO [Char]
-userName (TestCC ChatController {currentUser} _ _ _ _ _) = T.unpack . localDisplayName . fromJust <$> readTVarIO currentUser
+userName (TestCC ChatController {currentUser} _ _ _ _ _) = maybe "no current user" (T.unpack . localDisplayName) <$> readTVarIO currentUser
testChat2 :: HasCallStack => Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> IO ()) -> FilePath -> IO ()
testChat2 = testChatCfgOpts2 testCfg testOpts
diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs
index 7d299e296d..64fa6ff3bf 100644
--- a/tests/ChatTests/Direct.hs
+++ b/tests/ChatTests/Direct.hs
@@ -1492,16 +1492,16 @@ testDeleteUser =
\alice bob cath dan -> do
connectUsers alice bob
- -- cannot delete active user
+ alice ##> "/create user alisa"
+ showActiveUser alice "alisa"
- alice ##> "/_delete user 1 del_smp=off"
+ -- cannot delete active user when there is another user
+
+ alice ##> "/_delete user 2 del_smp=off"
alice <## "cannot delete active user"
-- delete user without deleting SMP queues
- alice ##> "/create user alisa"
- showActiveUser alice "alisa"
-
connectUsers alice cath
alice <##> cath
@@ -1519,17 +1519,7 @@ testDeleteUser =
-- no connection authorization error - connection wasn't deleted
(alice )
- -- cannot delete new active user
-
- alice ##> "/delete user alisa"
- alice <## "cannot delete active user"
-
- alice ##> "/users"
- alice <## "alisa (active)"
-
- alice <##> cath
-
- -- delete user deleting SMP queues
+ -- cannot delete active user when there is another user
alice ##> "/create user alisa2"
showActiveUser alice "alisa2"
@@ -1537,10 +1527,17 @@ testDeleteUser =
connectUsers alice dan
alice <##> dan
+ alice ##> "/delete user alisa2"
+ alice <## "cannot delete active user"
+
alice ##> "/users"
alice <## "alisa"
alice <## "alisa2 (active)"
+ alice <##> dan
+
+ -- delete user deleting SMP queues
+
alice ##> "/delete user alisa"
alice <### ["ok", "completed deleting user"]
@@ -1553,6 +1550,16 @@ testDeleteUser =
alice <##> dan
+ -- delete last active user
+
+ alice ##> "/delete user alisa2 del_smp=off"
+ alice <### ["ok", "completed deleting user"]
+ alice ##> "/users"
+ alice <## "no users"
+
+ alice ##> "/create user alisa3"
+ showActiveUser alice "alisa3"
+
testUsersDifferentCIExpirationTTL :: HasCallStack => FilePath -> IO ()
testUsersDifferentCIExpirationTTL tmp = do
withNewTestChat tmp "bob" bobProfile $ \bob -> do
@@ -2047,12 +2054,23 @@ testUserPrivacy =
userVisible alice "current "
alice ##> "/hide user new_password"
userHidden alice "current "
- alice ##> "/_delete user 1 del_smp=on"
- alice <## "cannot delete last user"
- alice ##> "/_hide user 1 \"password\""
- alice <## "cannot hide the only not hidden user"
alice ##> "/user alice"
showActiveUser alice "alice (Alice)"
+ -- delete last visible active user
+ alice ##> "/_delete user 1 del_smp=on"
+ alice <### ["ok", "completed deleting user"]
+ -- hidden user is not shown
+ alice ##> "/users"
+ alice <## "no users"
+ -- but it is still possible to switch to it
+ alice ##> "/user alisa wrong_password"
+ alice <## "user does not exist or incorrect password"
+ alice ##> "/user alisa new_password"
+ showActiveUser alice "alisa"
+ alice ##> "/create user alisa2"
+ showActiveUser alice "alisa2"
+ alice ##> "/_hide user 3 \"password2\""
+ alice <## "cannot hide the only not hidden user"
-- change profile privacy for inactive user via API requires correct password
alice ##> "/_unmute user 2"
alice <## "hidden user always muted when inactive"
@@ -2064,17 +2082,14 @@ testUserPrivacy =
userVisible alice ""
alice ##> "/_hide user 2 \"another_password\""
userHidden alice ""
- alice ##> "/user alisa another_password"
- showActiveUser alice "alisa"
- alice ##> "/user alice"
- showActiveUser alice "alice (Alice)"
alice ##> "/_delete user 2 del_smp=on"
alice <## "user does not exist or incorrect password"
alice ##> "/_delete user 2 del_smp=on \"wrong_password\""
alice <## "user does not exist or incorrect password"
alice ##> "/_delete user 2 del_smp=on \"another_password\""
- alice <## "ok"
- alice <## "completed deleting user"
+ alice <### ["ok", "completed deleting user"]
+ alice ##> "/_delete user 3 del_smp=on"
+ alice <### ["ok", "completed deleting user"]
where
userHidden alice current = do
alice <## (current <> "user alisa:")
From 6ba3100d348e23549245e2d435fa9815108584e4 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Wed, 20 Dec 2023 06:38:39 +0000
Subject: [PATCH 05/32] core: batch sending messages (#3566)
* core: batch sending messages
* batch without iorefs (#3573)
* one-pass
* simplexmq
* simplexmq
* simplexmq
* simplexmq
* revert change to ios project file
* refactor
* simplify
---------
Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com>
---
cabal.project | 2 +-
package.yaml | 2 +-
scripts/nix/sha256map.nix | 2 +-
simplex-chat.cabal | 14 ++---
src/Simplex/Chat.hs | 96 +++++++++++++++++++++-------------
src/Simplex/Chat/Controller.hs | 15 ++++++
tests/ChatClient.hs | 1 +
7 files changed, 87 insertions(+), 45 deletions(-)
diff --git a/cabal.project b/cabal.project
index 873035d7ab..e81c21c990 100644
--- a/cabal.project
+++ b/cabal.project
@@ -14,7 +14,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
- tag: 18be2709f59a4cb20fe9758b899622092dba062e
+ tag: 8c250ebe19f56dd7d53572d984e8016cb0e4d658
source-repository-package
type: git
diff --git a/package.yaml b/package.yaml
index af58ce6729..65f99a7a78 100644
--- a/package.yaml
+++ b/package.yaml
@@ -45,7 +45,7 @@ dependencies:
- sqlcipher-simple == 0.4.*
- stm == 2.5.*
- terminal == 0.2.*
- - time == 1.9.*
+ - time == 1.12.*
- tls >= 1.7.0 && < 1.8
- unliftio == 0.2.*
- unliftio-core == 0.2.*
diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix
index 3733163f49..9f06b66101 100644
--- a/scripts/nix/sha256map.nix
+++ b/scripts/nix/sha256map.nix
@@ -1,5 +1,5 @@
{
- "https://github.com/simplex-chat/simplexmq.git"."18be2709f59a4cb20fe9758b899622092dba062e" = "08dr4vyg1wz2z768iikg8fks5zqf4dw5myr87hbpv964idda3pmj";
+ "https://github.com/simplex-chat/simplexmq.git"."8c250ebe19f56dd7d53572d984e8016cb0e4d658" = "080rw86yncf1h3zr5a8y65cndihq6f3ji43vxrdhr2mrb75vmw8m";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
diff --git a/simplex-chat.cabal b/simplex-chat.cabal
index f3918dfecd..6462d26008 100644
--- a/simplex-chat.cabal
+++ b/simplex-chat.cabal
@@ -199,7 +199,7 @@ library
, sqlcipher-simple ==0.4.*
, stm ==2.5.*
, terminal ==0.2.*
- , time ==1.9.*
+ , time ==1.12.*
, tls >=1.7.0 && <1.8
, unliftio ==0.2.*
, unliftio-core ==0.2.*
@@ -259,7 +259,7 @@ executable simplex-bot
, sqlcipher-simple ==0.4.*
, stm ==2.5.*
, terminal ==0.2.*
- , time ==1.9.*
+ , time ==1.12.*
, tls >=1.7.0 && <1.8
, unliftio ==0.2.*
, unliftio-core ==0.2.*
@@ -319,7 +319,7 @@ executable simplex-bot-advanced
, sqlcipher-simple ==0.4.*
, stm ==2.5.*
, terminal ==0.2.*
- , time ==1.9.*
+ , time ==1.12.*
, tls >=1.7.0 && <1.8
, unliftio ==0.2.*
, unliftio-core ==0.2.*
@@ -381,7 +381,7 @@ executable simplex-broadcast-bot
, sqlcipher-simple ==0.4.*
, stm ==2.5.*
, terminal ==0.2.*
- , time ==1.9.*
+ , time ==1.12.*
, tls >=1.7.0 && <1.8
, unliftio ==0.2.*
, unliftio-core ==0.2.*
@@ -442,7 +442,7 @@ executable simplex-chat
, sqlcipher-simple ==0.4.*
, stm ==2.5.*
, terminal ==0.2.*
- , time ==1.9.*
+ , time ==1.12.*
, tls >=1.7.0 && <1.8
, unliftio ==0.2.*
, unliftio-core ==0.2.*
@@ -508,7 +508,7 @@ executable simplex-directory-service
, sqlcipher-simple ==0.4.*
, stm ==2.5.*
, terminal ==0.2.*
- , time ==1.9.*
+ , time ==1.12.*
, tls >=1.7.0 && <1.8
, unliftio ==0.2.*
, unliftio-core ==0.2.*
@@ -602,7 +602,7 @@ test-suite simplex-chat-test
, sqlcipher-simple ==0.4.*
, stm ==2.5.*
, terminal ==0.2.*
- , time ==1.9.*
+ , time ==1.12.*
, tls >=1.7.0 && <1.8
, unliftio ==0.2.*
, unliftio-core ==0.2.*
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 6b619c5bd6..4e7a1cab9a 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -35,7 +35,7 @@ import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Char
import Data.Constraint (Dict (..))
-import Data.Either (fromRight, partitionEithers, rights)
+import Data.Either (fromRight, lefts, partitionEithers, rights)
import Data.Fixed (div')
import Data.Functor (($>))
import Data.Int (Int64)
@@ -5002,7 +5002,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
Left _ -> messageError "x.grp.mem.inv error: referenced member does not exist"
Right reMember -> do
GroupMemberIntro {introId} <- withStore $ \db -> saveIntroInvitation db reMember m introInv
- void . sendGroupMessage' user [reMember] (XGrpMemFwd (memberInfo m) introInv) groupId (Just introId) $
+ sendGroupMemberMessage user reMember (XGrpMemFwd (memberInfo m) introInv) groupId (Just introId) $
withStore' $
\db -> updateIntroStatus db introId GMIntroInvForwarded
_ -> messageError "x.grp.mem.inv can be only sent by invitee member"
@@ -5529,46 +5529,62 @@ directMessage chatMsgEvent = do
pure $ strEncode ChatMessage {chatVRange, msgId = Nothing, chatMsgEvent}
deliverMessage :: ChatMonad m => Connection -> CMEventTag e -> MsgBody -> MessageId -> m Int64
-deliverMessage conn@Connection {connId} cmEventTag msgBody msgId = do
- let msgFlags = MsgFlags {notification = hasNotification cmEventTag}
- agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) msgFlags msgBody
- let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId}
- withStore' $ \db -> createSndMsgDelivery db sndMsgDelivery msgId
+deliverMessage conn cmEventTag msgBody msgId =
+ deliverMessages [(conn, cmEventTag, msgBody, msgId)] >>= \case
+ [r] -> liftEither r
+ rs -> throwChatError $ CEInternalError $ "deliverMessage: expected 1 result, got " <> show (length rs)
+
+deliverMessages :: ChatMonad' m => [(Connection, CMEventTag e, MsgBody, MessageId)] -> m [Either ChatError Int64]
+deliverMessages msgReqs = do
+ sent <- zipWith prepareBatch msgReqs <$> withAgent' (`sendMessages` aReqs)
+ withStoreBatch $ \db -> map (bindRight $ createDelivery db) sent
+ where
+ aReqs = map (\(conn, cmEvTag, msgBody, _msgId) -> (aConnId conn, msgFlags cmEvTag, msgBody)) msgReqs
+ msgFlags cmEvTag = MsgFlags {notification = hasNotification cmEvTag}
+ prepareBatch req = bimap (`ChatErrorAgent` Nothing) (req,)
+ createDelivery :: DB.Connection -> ((Connection, CMEventTag e, MsgBody, MessageId), AgentMsgId) -> IO (Either ChatError Int64)
+ createDelivery db ((Connection {connId}, _, _, msgId), agentMsgId) =
+ Right <$> createSndMsgDelivery db (SndMsgDelivery {connId, agentMsgId}) msgId
sendGroupMessage :: (MsgEncodingI e, ChatMonad m) => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> m (SndMessage, [GroupMember])
-sendGroupMessage user GroupInfo {groupId} members chatMsgEvent =
- sendGroupMessage' user members chatMsgEvent groupId Nothing $ pure ()
-
-sendGroupMessage' :: forall e m. (MsgEncodingI e, ChatMonad m) => User -> [GroupMember] -> ChatMsgEvent e -> Int64 -> Maybe Int64 -> m () -> m (SndMessage, [GroupMember])
-sendGroupMessage' user members chatMsgEvent groupId introId_ postDeliver = do
- msg <- createSndMessage chatMsgEvent (GroupId groupId)
- -- TODO collect failed deliveries into a single error
+sendGroupMessage user GroupInfo {groupId} members chatMsgEvent = do
+ msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent (GroupId groupId)
recipientMembers <- liftIO $ shuffleMembers (filter memberCurrent members) $ \GroupMember {memberRole} -> memberRole
- rs <- forM recipientMembers $ \m ->
- messageMember m msg `catchChatError` (\e -> toView (CRChatError (Just user) e) $> Nothing)
- let sentToMembers = catMaybes rs
+ let tag = toCMEventTag chatMsgEvent
+ (toSend, pending) = foldr addMember ([], []) recipientMembers
+ msgReqs = map (\(_, conn) -> (conn, tag, msgBody, msgId)) toSend
+ delivered <- deliverMessages msgReqs
+ let errors = lefts delivered
+ unless (null errors) $ toView $ CRChatErrors (Just user) errors
+ stored <- withStoreBatch' $ \db -> map (\m -> createPendingGroupMessage db (groupMemberId' m) msgId Nothing) pending
+ let sentToMembers = filterSent delivered toSend fst <> filterSent stored pending id
pure (msg, sentToMembers)
where
- messageMember :: GroupMember -> SndMessage -> m (Maybe GroupMember)
- messageMember m@GroupMember {groupMemberId} SndMessage {msgId, msgBody} = case memberConn m of
- Nothing -> pendingOrForwarded
- Just conn@Connection {connStatus}
- | connDisabled conn || connStatus == ConnDeleted -> pure Nothing
- | connStatus == ConnSndReady || connStatus == ConnReady -> do
- let tag = toCMEventTag chatMsgEvent
- deliverMessage conn tag msgBody msgId >> postDeliver
- pure $ Just m
- | otherwise -> pendingOrForwarded
+ addMember m (toSend, pending) = case memberSendAction chatMsgEvent members m of
+ Just (MSASend conn) -> ((m, conn) : toSend, pending)
+ Just MSAPending -> (toSend, m : pending)
+ Nothing -> (toSend, pending)
+ filterSent :: [Either ChatError a] -> [mem] -> (mem -> GroupMember) -> [GroupMember]
+ filterSent rs ms mem = [mem m | (Right _, m) <- zip rs ms]
+
+data MemberSendAction = MSASend Connection | MSAPending
+
+memberSendAction :: ChatMsgEvent e -> [GroupMember] -> GroupMember -> Maybe MemberSendAction
+memberSendAction chatMsgEvent members m = case memberConn m of
+ Nothing -> pendingOrForwarded
+ Just conn@Connection {connStatus}
+ | connDisabled conn || connStatus == ConnDeleted -> Nothing
+ | connStatus == ConnSndReady || connStatus == ConnReady -> Just (MSASend conn)
+ | otherwise -> pendingOrForwarded
+ where
+ pendingOrForwarded
+ | forwardSupported && isForwardedGroupMsg chatMsgEvent = Nothing
+ | isXGrpMsgForward chatMsgEvent = Nothing
+ | otherwise = Just MSAPending
where
- pendingOrForwarded
- | forwardSupported && isForwardedGroupMsg chatMsgEvent = pure Nothing
- | isXGrpMsgForward chatMsgEvent = pure Nothing
- | otherwise = do
- withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_
- pure $ Just m
- forwardSupported = do
+ forwardSupported =
let mcvr = memberChatVRange' m
- isCompatibleRange mcvr groupForwardVRange && invitingMemberSupportsForward
+ in isCompatibleRange mcvr groupForwardVRange && invitingMemberSupportsForward
invitingMemberSupportsForward = case m.invitedByGroupMemberId of
Just invMemberId ->
-- can be optimized for large groups by replacing [GroupMember] with Map GroupMemberId GroupMember
@@ -5582,6 +5598,16 @@ sendGroupMessage' user members chatMsgEvent groupId introId_ postDeliver = do
XGrpMsgForward {} -> True
_ -> False
+sendGroupMemberMessage :: forall e m. (MsgEncodingI e, ChatMonad m) => User -> GroupMember -> ChatMsgEvent e -> Int64 -> Maybe Int64 -> m () -> m ()
+sendGroupMemberMessage user m@GroupMember {groupMemberId} chatMsgEvent groupId introId_ postDeliver = do
+ msg <- createSndMessage chatMsgEvent (GroupId groupId)
+ messageMember msg `catchChatError` (\e -> toView (CRChatError (Just user) e))
+ where
+ messageMember :: SndMessage -> m ()
+ messageMember SndMessage {msgId, msgBody} = forM_ (memberSendAction chatMsgEvent [m] m) $ \case
+ MSASend conn -> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId >> postDeliver
+ MSAPending -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_
+
shuffleMembers :: [a] -> (a -> GroupMemberRole) -> IO [a]
shuffleMembers ms role = do
let (adminMs, otherMs) = partition ((GRAdmin <=) . role) ms
diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs
index 8446c15a81..70e0cc64fc 100644
--- a/src/Simplex/Chat/Controller.hs
+++ b/src/Simplex/Chat/Controller.hs
@@ -84,6 +84,7 @@ import Simplex.RemoteControl.Invitation (RCSignedInvitation, RCVerifiedInvitatio
import Simplex.RemoteControl.Types
import System.IO (Handle)
import System.Mem.Weak (Weak)
+import qualified UnliftIO.Exception as E
import UnliftIO.STM
versionNumber :: String
@@ -1287,12 +1288,26 @@ withStoreCtx ctx_ action = do
handleInternal :: String -> SomeException -> IO (Either StoreError a)
handleInternal ctxStr e = pure . Left . SEInternalError $ show e <> ctxStr
+withStoreBatch :: (ChatMonad' m, Traversable t) => (DB.Connection -> t (IO (Either ChatError a))) -> m (t (Either ChatError a))
+withStoreBatch actions = do
+ ChatController {chatStore} <- ask
+ liftIO $ withTransaction chatStore $ mapM (`E.catch` handleInternal) . actions
+ where
+ handleInternal :: E.SomeException -> IO (Either ChatError a)
+ handleInternal = pure . Left . ChatError . CEInternalError . show
+
+withStoreBatch' :: (ChatMonad' m, Traversable t) => (DB.Connection -> t (IO a)) -> m (t (Either ChatError a))
+withStoreBatch' actions = withStoreBatch $ fmap (fmap Right) . actions
+
withAgent :: ChatMonad m => (AgentClient -> ExceptT AgentErrorType m a) -> m a
withAgent action =
asks smpAgent
>>= runExceptT . action
>>= liftEither . first (`ChatErrorAgent` Nothing)
+withAgent' :: ChatMonad' m => (AgentClient -> m a) -> m a
+withAgent' action = asks smpAgent >>= action
+
$(JQ.deriveJSON (enumJSON $ dropPrefix "HS") ''HelpSection)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CLQ") ''ChatListQuery)
diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs
index 821d7b032b..c32d8002b9 100644
--- a/tests/ChatClient.hs
+++ b/tests/ChatClient.hs
@@ -353,6 +353,7 @@ serverCfg =
serverStatsBackupFile = Nothing,
smpServerVRange = supportedSMPServerVRange,
transportConfig = defaultTransportServerConfig,
+ smpHandshakeTimeout = 1000000,
controlPort = Nothing
}
From 4a4d470859e86b44ebf61447a505957c54ffcf5e Mon Sep 17 00:00:00 2001
From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
Date: Thu, 21 Dec 2023 02:00:44 +0800
Subject: [PATCH 06/32] android, desktop: try-catch composables (#3575)
* android, desktop: try-catch composables
* test
* better catching on Android
* more try-catch'es
* Revert "test"
This reverts commit adaf92b116fd8453d44cd401055fb0904f41f23c.
* more try-catch'es
* unneeded imports
---
.../main/java/chat/simplex/app/SimplexApp.kt | 17 ++++++
.../simplex/common/platform/UI.android.kt | 34 ++++++-----
.../kotlin/chat/simplex/common/App.kt | 8 ++-
.../simplex/common/views/chat/ChatView.kt | 6 +-
.../views/chat/item/CIBrokenComposableView.kt | 18 ++++++
.../views/chatlist/ChatListNavLinkView.kt | 60 ++++++++++++++++---
.../common/views/chatlist/ChatListView.kt | 26 ++++++--
.../common/views/chatlist/ShareListView.kt | 10 ++--
.../simplex/common/views/helpers/Utils.kt | 22 +++++++
.../commonMain/resources/MR/base/strings.xml | 3 +
.../kotlin/chat/simplex/common/DesktopApp.kt | 1 +
11 files changed, 169 insertions(+), 36 deletions(-)
create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIBrokenComposableView.kt
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 a345e6e48f..e3f4e69bd4 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
@@ -1,6 +1,8 @@
package chat.simplex.app
import android.app.Application
+import android.os.Handler
+import android.os.Looper
import chat.simplex.common.platform.Log
import androidx.lifecycle.*
import androidx.work.*
@@ -35,6 +37,21 @@ class SimplexApp: Application(), LifecycleEventObserver {
return
} else {
registerGlobalErrorHandler()
+ Handler(Looper.getMainLooper()).post {
+ while (true) {
+ try {
+ Looper.loop()
+ } catch (e: Throwable) {
+ if (e.message != null && e.message!!.startsWith("Unable to start activity")) {
+ android.os.Process.killProcess(android.os.Process.myPid())
+ break
+ } else {
+ // Send it to our exception handled because it will not get the exception otherwise
+ Thread.getDefaultUncaughtExceptionHandler()?.uncaughtException(Looper.getMainLooper().thread, e)
+ }
+ }
+ }
+ }
}
context = this
initHaskell()
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 96bb739113..371c140133 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
@@ -4,7 +4,7 @@ import android.app.Activity
import android.content.Context
import android.content.pm.ActivityInfo
import android.graphics.Rect
-import android.os.Build
+import android.os.*
import android.view.*
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
@@ -12,7 +12,6 @@ import androidx.activity.compose.setContent
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalView
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
@@ -79,6 +78,7 @@ actual fun androidIsFinishingMainActivity(): Boolean = (mainActivity.get()?.isFi
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())
+ includeMoreFailedComposables()
if (ModalManager.start.hasModalsOpen()) {
ModalManager.start.closeModal()
} else if (chatModel.chatId.value != null) {
@@ -93,19 +93,25 @@ actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler {
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()
+ if (thread.name == "main") {
+ mainActivity.get()?.recreate()
+ } else {
+ mainActivity.get()?.apply {
+ window
+ ?.decorView
+ ?.findViewById(android.R.id.content)
+ ?.removeViewAt(0)
+ setContent {
+ AppScreen()
+ }
}
}
+ // Wait until activity recreates to prevent showing two alerts (in case `main` was crashed)
+ Handler(Looper.getMainLooper()).post {
+ AlertManager.shared.showAlertMsg(
+ title = generalGetString(MR.strings.app_was_crashed),
+ text = e.stackTraceToString()
+ )
+ }
}
}
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 0082972c7a..d457eb57a1 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
@@ -332,9 +332,11 @@ fun DesktopScreen(settingsState: SettingsViewState) {
)
}
VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH))
- UserPicker(chatModel, userPickerState) {
- scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
- userPickerState.value = AnimatedViewState.GONE
+ tryOrShowError("UserPicker", error = {}) {
+ UserPicker(chatModel, userPickerState) {
+ scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
+ userPickerState.value = AnimatedViewState.GONE
+ }
}
ModalManager.fullscreen.showInView()
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt
index 8eee43035b..ebec780df7 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt
@@ -900,7 +900,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
@Composable
fun ChatItemViewShortHand(cItem: ChatItem, range: IntRange?) {
- ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools)
+ tryOrShowError("${cItem.id}ChatItem", error = {
+ CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart)
+ }) {
+ ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools)
+ }
}
@Composable
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIBrokenComposableView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIBrokenComposableView.kt
new file mode 100644
index 0000000000..d49f8526d5
--- /dev/null
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIBrokenComposableView.kt
@@ -0,0 +1,18 @@
+package chat.simplex.common.views.chat.item
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import dev.icerock.moko.resources.compose.stringResource
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.unit.dp
+import chat.simplex.res.MR
+
+@Composable
+fun CIBrokenComposableView(alignment: Alignment) {
+ Box(Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 6.dp), contentAlignment = alignment) {
+ Text(stringResource(MR.strings.error_showing_message), color = MaterialTheme.colors.error, fontStyle = FontStyle.Italic)
+ }
+}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt
index 9ae0da2a31..8d5446aa53 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt
@@ -14,6 +14,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -61,9 +62,17 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
is ChatInfo.Direct -> {
val contactNetworkStatus = chatModel.contactNetworkStatus(chat.chatInfo.contact)
ChatListNavLinkLayout(
- chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode, inProgress = false, progressByTimeout = false) },
+ chatLinkPreview = {
+ tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) {
+ ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode, inProgress = false, progressByTimeout = false)
+ }
+ },
click = { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) },
- dropdownMenuItems = { ContactMenuItems(chat, chat.chatInfo.contact, chatModel, showMenu, showMarkRead) },
+ dropdownMenuItems = {
+ tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) {
+ ContactMenuItems(chat, chat.chatInfo.contact, chatModel, showMenu, showMarkRead)
+ }
+ },
showMenu,
stopped,
selectedChat
@@ -71,25 +80,45 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
}
is ChatInfo.Group ->
ChatListNavLinkLayout(
- chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, stopped, linkMode, inProgress.value, progressByTimeout) },
+ chatLinkPreview = {
+ tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) {
+ ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, stopped, linkMode, inProgress.value, progressByTimeout)
+ }
+ },
click = { if (!inProgress.value) groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel, inProgress) },
- dropdownMenuItems = { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, inProgress, showMarkRead) },
+ dropdownMenuItems = {
+ tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) {
+ GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, inProgress, showMarkRead)
+ }
+ },
showMenu,
stopped,
selectedChat
)
is ChatInfo.ContactRequest ->
ChatListNavLinkLayout(
- chatLinkPreview = { ContactRequestView(chat.chatInfo) },
+ chatLinkPreview = {
+ tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) {
+ ContactRequestView(chat.chatInfo)
+ }
+ },
click = { contactRequestAlertDialog(chat.remoteHostId, chat.chatInfo, chatModel) },
- dropdownMenuItems = { ContactRequestMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu) },
+ dropdownMenuItems = {
+ tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) {
+ ContactRequestMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu)
+ }
+ },
showMenu,
stopped,
selectedChat
)
is ChatInfo.ContactConnection ->
ChatListNavLinkLayout(
- chatLinkPreview = { ContactConnectionView(chat.chatInfo.contactConnection) },
+ chatLinkPreview = {
+ tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) {
+ ContactConnectionView(chat.chatInfo.contactConnection)
+ }
+ },
click = {
ModalManager.center.closeModals()
ModalManager.end.closeModals()
@@ -97,7 +126,11 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
ContactConnectionInfoView(chatModel, chat.remoteHostId, chat.chatInfo.contactConnection.connReqInv, chat.chatInfo.contactConnection, false, close)
}
},
- dropdownMenuItems = { ContactConnectionMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu) },
+ dropdownMenuItems = {
+ tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) {
+ ContactConnectionMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu)
+ }
+ },
showMenu,
stopped,
selectedChat
@@ -105,7 +138,9 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
is ChatInfo.InvalidJSON ->
ChatListNavLinkLayout(
chatLinkPreview = {
- InvalidDataView()
+ tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) {
+ InvalidDataView()
+ }
},
click = {
ModalManager.end.closeModals()
@@ -119,6 +154,13 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
}
}
+@Composable
+private fun ErrorChatListItem() {
+ Box(Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 6.dp)) {
+ Text(stringResource(MR.strings.error_showing_content), color = MaterialTheme.colors.error, fontStyle = FontStyle.Italic)
+ }
+}
+
fun directChatAction(rhId: Long?, contact: Contact, chatModel: ChatModel) {
when {
contact.activeConn == null && contact.profile.contactLink != null -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close = null, openChat = true)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt
index 18252d0e22..cf12727d74 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt
@@ -11,6 +11,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.*
+import androidx.compose.ui.text.font.FontStyle
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.font.FontWeight
@@ -64,7 +65,11 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
val (userPickerState, scaffoldState ) = settingsState
Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(chatModel, scaffoldState.drawerState, userPickerState, stopped) { searchInList = it.trim() } } },
scaffoldState = scaffoldState,
- drawerContent = { SettingsView(chatModel, setPerformLA, scaffoldState.drawerState) },
+ drawerContent = {
+ tryOrShowError("Settings", error = { ErrorSettingsView() }) {
+ SettingsView(chatModel, setPerformLA, scaffoldState.drawerState)
+ }
+ },
drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f),
drawerGesturesEnabled = appPlatform.isAndroid,
floatingActionButton = {
@@ -111,12 +116,16 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
if (searchInList.isEmpty()) {
DesktopActiveCallOverlayLayout(newChatSheetState)
// TODO disable this button and sheet for the duration of the switch
- NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
+ tryOrShowError("NewChatSheet", error = {}) {
+ NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
+ }
}
if (appPlatform.isAndroid) {
- UserPicker(chatModel, userPickerState) {
- scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
- userPickerState.value = AnimatedViewState.GONE
+ tryOrShowError("UserPicker", error = {}) {
+ UserPicker(chatModel, userPickerState) {
+ scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
+ userPickerState.value = AnimatedViewState.GONE
+ }
}
}
}
@@ -303,6 +312,13 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) {
}
}
+@Composable
+private fun ErrorSettingsView() {
+ Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Text(generalGetString(MR.strings.error_showing_content), color = MaterialTheme.colors.error, fontStyle = FontStyle.Italic)
+ }
+}
+
private var lazyListState = 0 to 0
@Composable
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt
index 8338d2960f..ac8331007e 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt
@@ -47,10 +47,12 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
}
}
if (appPlatform.isAndroid) {
- UserPicker(chatModel, userPickerState, showSettings = false, showCancel = true, cancelClicked = {
- chatModel.sharedContent.value = null
- userPickerState.value = AnimatedViewState.GONE
- })
+ tryOrShowError("UserPicker", error = {}) {
+ UserPicker(chatModel, userPickerState, showSettings = false, showCancel = true, cancelClicked = {
+ chatModel.sharedContent.value = null
+ userPickerState.value = AnimatedViewState.GONE
+ })
+ }
}
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt
index 0a0ef17c4b..9a81b9f9d7 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt
@@ -390,6 +390,28 @@ fun IntSize.Companion.Saver(): Saver = Saver(
restore = { IntSize(it.first, it.second) }
)
+private var lastExecutedComposables = HashSet()
+private val failedComposables = HashSet()
+
+@Composable
+fun tryOrShowError(key: Any = Exception().stackTraceToString().lines()[2], error: @Composable () -> Unit = {}, content: @Composable () -> Unit) {
+ if (!failedComposables.contains(key)) {
+ lastExecutedComposables.add(key)
+ content()
+ lastExecutedComposables.remove(key)
+ } else {
+ error()
+ }
+}
+
+fun includeMoreFailedComposables() {
+ lastExecutedComposables.forEach {
+ failedComposables.add(it)
+ Log.i(TAG, "Added composable key as failed: $it")
+ }
+ lastExecutedComposables.clear()
+}
+
@Composable
fun DisposableEffectOnGone(always: () -> Unit = {}, whenDispose: () -> Unit = {}, whenGone: () -> Unit) {
DisposableEffect(Unit) {
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 e0b8f130db..7ee86c2f53 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
@@ -45,6 +45,9 @@
moderated
invalid chat
invalid data
+ error showing message
+ error showing content
+
Decryption error
Encryption re-negotiation error
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 12bead3663..57371e25a7 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
@@ -45,6 +45,7 @@ fun showApp() {
Log.e(TAG, "App crashed, thread name: " + Thread.currentThread().name + ", exception: " + e.stackTraceToString())
window.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSING))
closedByError.value = true
+ includeMoreFailedComposables()
// If the left side of screen has open modal, it's probably caused the crash
if (ModalManager.start.hasModalsOpen()) {
ModalManager.start.closeModal()
From 7bcda7e54b8cb3b19bf09d58a62bb7714d7757d9 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Thu, 21 Dec 2023 00:42:40 +0000
Subject: [PATCH 07/32] core: use ChaChaDRG as the source of randomness (#3551)
* core: use ChaChaDRG as the source of randomness
* do not use entropy directly
* dont use RNG from agent
* simplexmq
* update iOS
---
apps/ios/Shared/Views/Call/WebRTCClient.swift | 3 +-
apps/ios/SimpleXChat/CryptoFile.swift | 4 +-
apps/ios/SimpleXChat/SimpleX.h | 6 +--
cabal.project | 2 +-
scripts/nix/sha256map.nix | 2 +-
src/Simplex/Chat.hs | 35 ++++++++--------
src/Simplex/Chat/Controller.hs | 2 +-
src/Simplex/Chat/Mobile.hs | 6 +--
src/Simplex/Chat/Mobile/File.hs | 31 ++++++++------
src/Simplex/Chat/Mobile/WebRTC.hs | 15 ++++---
src/Simplex/Chat/Remote.hs | 4 +-
src/Simplex/Chat/Remote/Protocol.hs | 4 +-
src/Simplex/Chat/Remote/Transport.hs | 2 +-
src/Simplex/Chat/Store/Shared.hs | 8 ++--
tests/ChatTests/Files.hs | 2 +-
tests/MobileTests.hs | 40 ++++++++++++-------
tests/RemoteTests.hs | 10 -----
tests/Test.hs | 7 ++--
tests/WebRTCTests.hs | 31 +++++++++-----
19 files changed, 120 insertions(+), 94 deletions(-)
diff --git a/apps/ios/Shared/Views/Call/WebRTCClient.swift b/apps/ios/Shared/Views/Call/WebRTCClient.swift
index acb459938f..933a3c745e 100644
--- a/apps/ios/Shared/Views/Call/WebRTCClient.swift
+++ b/apps/ios/Shared/Views/Call/WebRTCClient.swift
@@ -18,6 +18,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
}()
private static let ivTagBytes: Int = 28
private static let enableEncryption: Bool = true
+ private var chat_ctrl = getChatCtrl()
struct Call {
var connection: RTCPeerConnection
@@ -308,7 +309,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
memcpy(pointer, (unencrypted as NSData).bytes, unencrypted.count)
let isKeyFrame = unencrypted[0] & 1 == 0
let clearTextBytesSize = mediaType.rawValue == 0 ? 1 : isKeyFrame ? 10 : 3
- logCrypto("encrypt", chat_encrypt_media(&key, pointer.advanced(by: clearTextBytesSize), Int32(unencrypted.count + WebRTCClient.ivTagBytes - clearTextBytesSize)))
+ logCrypto("encrypt", chat_encrypt_media(chat_ctrl, &key, pointer.advanced(by: clearTextBytesSize), Int32(unencrypted.count + WebRTCClient.ivTagBytes - clearTextBytesSize)))
return Data(bytes: pointer, count: unencrypted.count + WebRTCClient.ivTagBytes)
} else {
return nil
diff --git a/apps/ios/SimpleXChat/CryptoFile.swift b/apps/ios/SimpleXChat/CryptoFile.swift
index dcb2be9ae0..0e539ba97c 100644
--- a/apps/ios/SimpleXChat/CryptoFile.swift
+++ b/apps/ios/SimpleXChat/CryptoFile.swift
@@ -17,7 +17,7 @@ public func writeCryptoFile(path: String, data: Data) throws -> CryptoFileArgs {
let ptr: UnsafeMutableRawPointer = malloc(data.count)
memcpy(ptr, (data as NSData).bytes, data.count)
var cPath = path.cString(using: .utf8)!
- let cjson = chat_write_file(&cPath, ptr, Int32(data.count))!
+ let cjson = chat_write_file(getChatCtrl(), &cPath, ptr, Int32(data.count))!
let d = fromCString(cjson).data(using: .utf8)!
switch try jsonDecoder.decode(WriteFileResult.self, from: d) {
case let .result(cfArgs): return cfArgs
@@ -50,7 +50,7 @@ public func readCryptoFile(path: String, cryptoArgs: CryptoFileArgs) throws -> D
public func encryptCryptoFile(fromPath: String, toPath: String) throws -> CryptoFileArgs {
var cFromPath = fromPath.cString(using: .utf8)!
var cToPath = toPath.cString(using: .utf8)!
- let cjson = chat_encrypt_file(&cFromPath, &cToPath)!
+ let cjson = chat_encrypt_file(getChatCtrl(), &cFromPath, &cToPath)!
let d = fromCString(cjson).data(using: .utf8)!
switch try jsonDecoder.decode(WriteFileResult.self, from: d) {
case let .result(cfArgs): return cfArgs
diff --git a/apps/ios/SimpleXChat/SimpleX.h b/apps/ios/SimpleXChat/SimpleX.h
index 6e37a51779..909d76a76c 100644
--- a/apps/ios/SimpleXChat/SimpleX.h
+++ b/apps/ios/SimpleXChat/SimpleX.h
@@ -25,11 +25,11 @@ extern char *chat_parse_markdown(char *str);
extern char *chat_parse_server(char *str);
extern char *chat_password_hash(char *pwd, char *salt);
extern char *chat_valid_name(char *name);
-extern char *chat_encrypt_media(char *key, char *frame, int len);
+extern char *chat_encrypt_media(chat_ctrl ctl, char *key, char *frame, int len);
extern char *chat_decrypt_media(char *key, char *frame, int len);
// chat_write_file returns null-terminated string with JSON of WriteFileResult
-extern char *chat_write_file(char *path, char *data, int len);
+extern char *chat_write_file(chat_ctrl ctl, char *path, char *data, int len);
// chat_read_file returns a buffer with:
// result status (1 byte), then if
@@ -38,7 +38,7 @@ extern char *chat_write_file(char *path, char *data, int len);
extern char *chat_read_file(char *path, char *key, char *nonce);
// chat_encrypt_file returns null-terminated string with JSON of WriteFileResult
-extern char *chat_encrypt_file(char *fromPath, char *toPath);
+extern char *chat_encrypt_file(chat_ctrl ctl, char *fromPath, char *toPath);
// chat_decrypt_file returns null-terminated string with the error message
extern char *chat_decrypt_file(char *fromPath, char *key, char *nonce, char *toPath);
diff --git a/cabal.project b/cabal.project
index e81c21c990..1ff8aacd77 100644
--- a/cabal.project
+++ b/cabal.project
@@ -14,7 +14,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
- tag: 8c250ebe19f56dd7d53572d984e8016cb0e4d658
+ tag: 13a60d1d3944aa175311563e661161e759b92563
source-repository-package
type: git
diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix
index 9f06b66101..595d40c4e7 100644
--- a/scripts/nix/sha256map.nix
+++ b/scripts/nix/sha256map.nix
@@ -1,5 +1,5 @@
{
- "https://github.com/simplex-chat/simplexmq.git"."8c250ebe19f56dd7d53572d984e8016cb0e4d658" = "080rw86yncf1h3zr5a8y65cndihq6f3ji43vxrdhr2mrb75vmw8m";
+ "https://github.com/simplex-chat/simplexmq.git"."13a60d1d3944aa175311563e661161e759b92563" = "08mvqrbjfnq7c6mhkj4hhy4cxn0cj21n49lqzh67ani71g2g1xwa";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 4e7a1cab9a..8bce204f54 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -22,7 +22,6 @@ import Control.Monad
import Control.Monad.Except
import Control.Monad.IO.Unlift
import Control.Monad.Reader
-import Crypto.Random (drgNew)
import qualified Data.Aeson as J
import Data.Attoparsec.ByteString.Char8 (Parser)
import qualified Data.Attoparsec.ByteString.Char8 as A
@@ -208,7 +207,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen
servers <- agentServers config
smpAgent <- getSMPAgentClient aCfg {tbqSize} servers agentStore
agentAsync <- newTVarIO Nothing
- idsDrg <- newTVarIO =<< liftIO drgNew
+ random <- liftIO C.newRandom
inputQ <- newTBQueueIO tbqSize
outputQ <- newTBQueueIO tbqSize
connNetworkStatuses <- atomically TM.empty
@@ -243,7 +242,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen
agentAsync,
chatStore,
chatStoreChanged,
- idsDrg,
+ random,
inputQ,
outputQ,
connNetworkStatuses,
@@ -1077,8 +1076,9 @@ processChatCommand = \case
then do
calls <- asks currentCalls
withChatLock "sendCallInvitation" $ do
- callId <- CallId <$> drgRandomBytes 16
- dhKeyPair <- if encryptedCall callType then Just <$> liftIO C.generateKeyPair' else pure Nothing
+ g <- asks random
+ callId <- atomically $ CallId <$> C.randomBytes 16 g
+ dhKeyPair <- atomically $ if encryptedCall callType then Just <$> C.generateKeyPair g else pure Nothing
let invitation = CallInvitation {callType, callDhPubKey = fst <$> dhKeyPair}
callState = CallInvitationSent {localCallType = callType, localDhPrivKey = snd <$> dhKeyPair}
(msg, _) <- sendDirectContactMessage ct (XCallInv callId invitation)
@@ -1600,7 +1600,7 @@ processChatCommand = \case
processChatCommand $ APIChatItemReaction chatRef chatItemId add reaction
APINewGroup userId incognito gProfile@GroupProfile {displayName} -> withUserId userId $ \user -> do
checkValidName displayName
- gVar <- asks idsDrg
+ gVar <- asks random
-- [incognito] generate incognito profile for group membership
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
groupInfo <- withStore $ \db -> createNewGroup db gVar user gProfile incognitoProfile
@@ -1621,7 +1621,7 @@ processChatCommand = \case
let sendInvitation = sendGrpInvitation user contact gInfo
case contactMember contact members of
Nothing -> do
- gVar <- asks idsDrg
+ gVar <- asks random
subMode <- chatReadVar subscriptionMode
(agentConnId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing subMode
member <- withStore $ \db -> createNewContactMember db gVar user gInfo contact memRole agentConnId cReq subMode
@@ -1884,7 +1884,7 @@ processChatCommand = \case
SetFileToReceive fileId encrypted_ -> withUser $ \_ -> do
withChatLock "setFileToReceive" . procCmd $ do
encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles
- cfArgs <- if encrypt then Just <$> liftIO CF.randomArgs else pure Nothing
+ cfArgs <- if encrypt then Just <$> (atomically . CF.randomArgs =<< asks random) else pure Nothing
withStore' $ \db -> setRcvFileToReceive db fileId cfArgs
ok_
CancelFile fileId -> withUser $ \user@User {userId} ->
@@ -2030,7 +2030,7 @@ processChatCommand = \case
-- in View.hs `r'` should be defined as `id` in this case
-- procCmd :: m ChatResponse -> m ChatResponse
-- procCmd action = do
- -- ChatController {chatLock = l, smpAgent = a, outputQ = q, idsDrg = gVar} <- ask
+ -- ChatController {chatLock = l, smpAgent = a, outputQ = q, random = gVar} <- ask
-- corrId <- liftIO $ SMP.CorrId <$> randomBytes gVar 8
-- void . forkIO $
-- withAgentLock a . withLock l name $
@@ -2296,7 +2296,7 @@ processChatCommand = \case
then pure Nothing
else Just . addUTCTime (realToFrac ttl) <$> liftIO getCurrentTime
drgRandomBytes :: Int -> m ByteString
- drgRandomBytes n = asks idsDrg >>= liftIO . (`randomBytes` n)
+ drgRandomBytes n = asks random >>= atomically . C.randomBytes n
privateGetUser :: UserId -> m User
privateGetUser userId =
tryChatError (withStore (`getUser` userId)) >>= \case
@@ -2571,7 +2571,7 @@ toFSFilePath f =
setFileToEncrypt :: ChatMonad m => RcvFileTransfer -> m RcvFileTransfer
setFileToEncrypt ft@RcvFileTransfer {fileId} = do
- cfArgs <- liftIO CF.randomArgs
+ cfArgs <- atomically . CF.randomArgs =<< asks random
withStore' $ \db -> setFileCryptoArgs db fileId cfArgs
pure (ft :: RcvFileTransfer) {cryptoArgs = Just cfArgs}
@@ -2726,7 +2726,7 @@ acceptGroupJoinRequestAsync
ucr@UserContactRequest {agentInvitationId = AgentInvId invId}
gLinkMemRole
incognitoProfile = do
- gVar <- asks idsDrg
+ gVar <- asks random
(groupMemberId, memberId) <- withStore $ \db -> createAcceptedMember db gVar user gInfo ucr gLinkMemRole
let Profile {displayName} = profileToSendOnAccept user incognitoProfile
GroupMember {memberRole = userRole, memberId = userMemberId} = membership
@@ -3407,7 +3407,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
groupInfo <- withStore $ \db -> getGroupInfo db user groupId
subMode <- chatReadVar subscriptionMode
groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation subMode
- gVar <- asks idsDrg
+ gVar <- asks random
withStore $ \db -> createNewContactMemberAsync db gVar user groupInfo ct gLinkMemRole groupConnIds (fromJVersionRange peerChatVRange) subMode
Just (gInfo, m@GroupMember {activeConn}) ->
when (maybe False ((== ConnReady) . connStatus) activeConn) $ do
@@ -4049,7 +4049,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
probeMatchingContactsAndMembers :: Contact -> IncognitoEnabled -> Bool -> m ()
probeMatchingContactsAndMembers ct connectedIncognito doProbeContacts = do
- gVar <- asks idsDrg
+ gVar <- asks random
contactMerge <- readTVarIO =<< asks contactMergeEnabled
if contactMerge && not connectedIncognito
then do
@@ -4073,7 +4073,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
probeMatchingMemberContact :: GroupMember -> IncognitoEnabled -> m ()
probeMatchingMemberContact GroupMember {activeConn = Nothing} _ = pure ()
probeMatchingMemberContact m@GroupMember {groupId, activeConn = Just conn} connectedIncognito = do
- gVar <- asks idsDrg
+ gVar <- asks random
contactMerge <- readTVarIO =<< asks contactMergeEnabled
if contactMerge && not connectedIncognito
then do
@@ -4774,7 +4774,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
if featureAllowed SCFCalls forContact ct
then do
- dhKeyPair <- if encryptedCall callType then Just <$> liftIO C.generateKeyPair' else pure Nothing
+ g <- asks random
+ dhKeyPair <- atomically $ if encryptedCall callType then Just <$> C.generateKeyPair g else pure Nothing
ci <- saveCallItem CISCallPending
let sharedKey = C.Key . C.dhBytes' <$> (C.dh' <$> callDhPubKey <*> (snd <$> dhKeyPair))
callState = CallInvitationReceived {peerCallType = callType, localDhPubKey = fst <$> dhKeyPair, sharedKey}
@@ -5517,7 +5518,7 @@ sendDirectMessage conn chatMsgEvent connOrGroupId = do
createSndMessage :: (MsgEncodingI e, ChatMonad m) => ChatMsgEvent e -> ConnOrGroupId -> m SndMessage
createSndMessage chatMsgEvent connOrGroupId = do
- gVar <- asks idsDrg
+ gVar <- asks random
ChatConfig {chatVRange} <- asks config
withStore $ \db -> createNewSndMessage db gVar connOrGroupId $ \sharedMsgId ->
let msgBody = strEncode ChatMessage {chatVRange, msgId = Just sharedMsgId, chatMsgEvent}
diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs
index 70e0cc64fc..b198cccbf7 100644
--- a/src/Simplex/Chat/Controller.hs
+++ b/src/Simplex/Chat/Controller.hs
@@ -180,7 +180,7 @@ data ChatController = ChatController
agentAsync :: TVar (Maybe (Async (), Maybe (Async ()))),
chatStore :: SQLiteStore,
chatStoreChanged :: TVar Bool, -- if True, chat should be fully restarted
- idsDrg :: TVar ChaChaDRG,
+ random :: TVar ChaChaDRG,
inputQ :: TBQueue String,
outputQ :: TBQueue (Maybe CorrId, Maybe RemoteHostId, ChatResponse),
connNetworkStatuses :: TMap AgentConnId NetworkStatus,
diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs
index a7f032c75b..6540352a3d 100644
--- a/src/Simplex/Chat/Mobile.hs
+++ b/src/Simplex/Chat/Mobile.hs
@@ -94,15 +94,15 @@ foreign export ccall "chat_password_hash" cChatPasswordHash :: CString -> CStrin
foreign export ccall "chat_valid_name" cChatValidName :: CString -> IO CString
-foreign export ccall "chat_encrypt_media" cChatEncryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString
+foreign export ccall "chat_encrypt_media" cChatEncryptMedia :: StablePtr ChatController -> CString -> Ptr Word8 -> CInt -> IO CString
foreign export ccall "chat_decrypt_media" cChatDecryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString
-foreign export ccall "chat_write_file" cChatWriteFile :: CString -> Ptr Word8 -> CInt -> IO CJSONString
+foreign export ccall "chat_write_file" cChatWriteFile :: StablePtr ChatController -> CString -> Ptr Word8 -> CInt -> IO CJSONString
foreign export ccall "chat_read_file" cChatReadFile :: CString -> CString -> CString -> IO (Ptr Word8)
-foreign export ccall "chat_encrypt_file" cChatEncryptFile :: CString -> CString -> IO CJSONString
+foreign export ccall "chat_encrypt_file" cChatEncryptFile :: StablePtr ChatController -> CString -> CString -> IO CJSONString
foreign export ccall "chat_decrypt_file" cChatDecryptFile :: CString -> CString -> CString -> CString -> IO CString
diff --git a/src/Simplex/Chat/Mobile/File.hs b/src/Simplex/Chat/Mobile/File.hs
index 1da64a3044..afbb1bc8c9 100644
--- a/src/Simplex/Chat/Mobile/File.hs
+++ b/src/Simplex/Chat/Mobile/File.hs
@@ -1,5 +1,6 @@
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
@@ -31,7 +32,9 @@ import Data.Word (Word32, Word8)
import Foreign.C
import Foreign.Marshal.Alloc (mallocBytes)
import Foreign.Ptr
+import Foreign.StablePtr
import Foreign.Storable (poke, pokeByteOff)
+import Simplex.Chat.Controller (ChatController (..))
import Simplex.Chat.Mobile.Shared
import Simplex.Chat.Util (chunkSize, encryptFile)
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..), CryptoFileHandle, FTCryptoError (..))
@@ -39,7 +42,7 @@ import qualified Simplex.Messaging.Crypto.File as CF
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
import Simplex.Messaging.Util (catchAll)
-import UnliftIO (Handle, IOMode (..), withFile)
+import UnliftIO (Handle, IOMode (..), atomically, withFile)
data WriteFileResult
= WFResult {cryptoArgs :: CryptoFileArgs}
@@ -47,16 +50,17 @@ data WriteFileResult
$(JQ.deriveToJSON (sumTypeJSON $ dropPrefix "WF") ''WriteFileResult)
-cChatWriteFile :: CString -> Ptr Word8 -> CInt -> IO CJSONString
-cChatWriteFile cPath ptr len = do
+cChatWriteFile :: StablePtr ChatController -> CString -> Ptr Word8 -> CInt -> IO CJSONString
+cChatWriteFile cc cPath ptr len = do
+ c <- deRefStablePtr cc
path <- peekCString cPath
s <- getByteString ptr len
- r <- chatWriteFile path s
+ r <- chatWriteFile c path s
newCStringFromLazyBS $ J.encode r
-chatWriteFile :: FilePath -> ByteString -> IO WriteFileResult
-chatWriteFile path s = do
- cfArgs <- CF.randomArgs
+chatWriteFile :: ChatController -> FilePath -> ByteString -> IO WriteFileResult
+chatWriteFile ChatController {random} path s = do
+ cfArgs <- atomically $ CF.randomArgs random
let file = CryptoFile path $ Just cfArgs
either WFError (\_ -> WFResult cfArgs)
<$> runCatchExceptT (withExceptT show $ CF.writeFile file $ LB.fromStrict s)
@@ -87,19 +91,20 @@ chatReadFile path keyStr nonceStr = runCatchExceptT $ do
let file = CryptoFile path $ Just $ CFArgs key nonce
withExceptT show $ CF.readFile file
-cChatEncryptFile :: CString -> CString -> IO CJSONString
-cChatEncryptFile cFromPath cToPath = do
+cChatEncryptFile :: StablePtr ChatController -> CString -> CString -> IO CJSONString
+cChatEncryptFile cc cFromPath cToPath = do
+ c <- deRefStablePtr cc
fromPath <- peekCString cFromPath
toPath <- peekCString cToPath
- r <- chatEncryptFile fromPath toPath
+ r <- chatEncryptFile c fromPath toPath
newCAString . LB'.unpack $ J.encode r
-chatEncryptFile :: FilePath -> FilePath -> IO WriteFileResult
-chatEncryptFile fromPath toPath =
+chatEncryptFile :: ChatController -> FilePath -> FilePath -> IO WriteFileResult
+chatEncryptFile ChatController {random} fromPath toPath =
either WFError WFResult <$> runCatchExceptT encrypt
where
encrypt = do
- cfArgs <- liftIO CF.randomArgs
+ cfArgs <- atomically $ CF.randomArgs random
encryptFile fromPath toPath cfArgs
pure cfArgs
diff --git a/src/Simplex/Chat/Mobile/WebRTC.hs b/src/Simplex/Chat/Mobile/WebRTC.hs
index 422cfd5a8c..537388b18b 100644
--- a/src/Simplex/Chat/Mobile/WebRTC.hs
+++ b/src/Simplex/Chat/Mobile/WebRTC.hs
@@ -1,4 +1,5 @@
{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE NamedFieldPuns #-}
module Simplex.Chat.Mobile.WebRTC
( cChatEncryptMedia,
@@ -21,11 +22,14 @@ import Data.Either (fromLeft)
import Data.Word (Word8)
import Foreign.C (CInt, CString, newCAString)
import Foreign.Ptr (Ptr)
+import Foreign.StablePtr
+import Simplex.Chat.Controller (ChatController (..))
import Simplex.Chat.Mobile.Shared
import qualified Simplex.Messaging.Crypto as C
+import UnliftIO (atomically)
-cChatEncryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString
-cChatEncryptMedia = cTransformMedia chatEncryptMedia
+cChatEncryptMedia :: StablePtr ChatController -> CString -> Ptr Word8 -> CInt -> IO CString
+cChatEncryptMedia = cTransformMedia . chatEncryptMedia
cChatDecryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString
cChatDecryptMedia = cTransformMedia chatDecryptMedia
@@ -39,11 +43,12 @@ cTransformMedia f cKey cFrame cFrameLen = do
putFrame s = when (B.length s <= fromIntegral cFrameLen) $ putByteString cFrame s
{-# INLINE cTransformMedia #-}
-chatEncryptMedia :: ByteString -> ByteString -> ExceptT String IO ByteString
-chatEncryptMedia keyStr frame = do
+chatEncryptMedia :: StablePtr ChatController -> ByteString -> ByteString -> ExceptT String IO ByteString
+chatEncryptMedia cc keyStr frame = do
+ ChatController {random} <- liftIO $ deRefStablePtr cc
len <- checkFrameLen frame
key <- decodeKey keyStr
- iv <- liftIO C.randomGCMIV
+ iv <- atomically $ C.randomGCMIV random
(tag, frame') <- withExceptT show $ C.encryptAESNoPad key iv $ B.take len frame
pure $ frame' <> BA.convert (C.unAuthTag tag) <> C.unGCMIV iv
diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs
index 3d98eb7e35..f3d0ba4d1b 100644
--- a/src/Simplex/Chat/Remote.hs
+++ b/src/Simplex/Chat/Remote.hs
@@ -142,7 +142,7 @@ startRemoteHost rh_ rcAddrPrefs_ port_ = do
Just (rhId, multicast) -> do
rh@RemoteHost {hostPairing} <- withStore $ \db -> getRemoteHost db rhId
pure (RHId rhId, multicast, Just $ remoteHostInfo rh $ Just RHSStarting, hostPairing) -- get from the database, start multicast if requested
- Nothing -> (RHNew,False,Nothing,) <$> rcNewHostPairing
+ Nothing -> withAgent $ \a -> (RHNew,False,Nothing,) <$> rcNewHostPairing a
sseq <- startRemoteHostSession rhKey
ctrlAppInfo <- mkCtrlAppInfo
(localAddrs, invitation, rchClient, vars) <- handleConnectError rhKey sseq . withAgent $ \a -> rcConnectHost a pairing (J.toJSON ctrlAppInfo) multicast rcAddrPrefs_ port_
@@ -352,7 +352,7 @@ storeRemoteFile rhId encrypted_ localPath = do
tmpDir <- getChatTempDirectory
createDirectoryIfMissing True tmpDir
tmpFile <- tmpDir `uniqueCombine` takeFileName localPath
- cfArgs <- liftIO CF.randomArgs
+ cfArgs <- atomically . CF.randomArgs =<< asks random
liftError (ChatError . CEFileWrite tmpFile) $ encryptFile localPath tmpFile cfArgs
pure $ CryptoFile tmpFile $ Just cfArgs
diff --git a/src/Simplex/Chat/Remote/Protocol.hs b/src/Simplex/Chat/Remote/Protocol.hs
index af4c7d33ec..b8ff847091 100644
--- a/src/Simplex/Chat/Remote/Protocol.hs
+++ b/src/Simplex/Chat/Remote/Protocol.hs
@@ -78,7 +78,7 @@ $(deriveJSON (taggedObjectJSON $ dropPrefix "RR") ''RemoteResponse)
mkRemoteHostClient :: ChatMonad m => HTTP2Client -> HostSessKeys -> SessionCode -> FilePath -> HostAppInfo -> m RemoteHostClient
mkRemoteHostClient httpClient sessionKeys sessionCode storePath HostAppInfo {encoding, deviceName, encryptFiles} = do
- drg <- asks $ agentDRG . smpAgent
+ drg <- asks random
counter <- newTVarIO 1
let HostSessKeys {hybridKey, idPrivKey, sessPrivKey} = sessionKeys
signatures = RSSign {idPrivKey, sessPrivKey}
@@ -95,7 +95,7 @@ mkRemoteHostClient httpClient sessionKeys sessionCode storePath HostAppInfo {enc
mkCtrlRemoteCrypto :: ChatMonad m => CtrlSessKeys -> SessionCode -> m RemoteCrypto
mkCtrlRemoteCrypto CtrlSessKeys {hybridKey, idPubKey, sessPubKey} sessionCode = do
- drg <- asks $ agentDRG . smpAgent
+ drg <- asks random
counter <- newTVarIO 1
let signatures = RSVerify {idPubKey, sessPubKey}
pure RemoteCrypto {drg, counter, sessionCode, hybridKey, signatures}
diff --git a/src/Simplex/Chat/Remote/Transport.hs b/src/Simplex/Chat/Remote/Transport.hs
index ccd10b328a..1c9c3f08eb 100644
--- a/src/Simplex/Chat/Remote/Transport.hs
+++ b/src/Simplex/Chat/Remote/Transport.hs
@@ -24,7 +24,7 @@ type EncryptedFile = ((Handle, Word32), C.CbNonce, LC.SbState)
prepareEncryptedFile :: RemoteCrypto -> (Handle, Word32) -> ExceptT RemoteProtocolError IO EncryptedFile
prepareEncryptedFile RemoteCrypto {drg, hybridKey} f = do
- nonce <- atomically $ C.pseudoRandomCbNonce drg
+ nonce <- atomically $ C.randomCbNonce drg
sbState <- liftEitherWith (const $ PRERemoteControl RCEEncrypt) $ LC.kcbInit hybridKey nonce
pure (f, nonce, sbState)
diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs
index e1125adc3a..1e69d70767 100644
--- a/src/Simplex/Chat/Store/Shared.hs
+++ b/src/Simplex/Chat/Store/Shared.hs
@@ -15,7 +15,7 @@ import qualified Control.Exception as E
import Control.Monad
import Control.Monad.Except
import Control.Monad.IO.Class
-import Crypto.Random (ChaChaDRG, randomBytesGenerate)
+import Crypto.Random (ChaChaDRG)
import qualified Data.Aeson.TH as J
import qualified Data.ByteString.Base64 as B64
import Data.ByteString.Char8 (ByteString)
@@ -35,6 +35,7 @@ import Simplex.Chat.Types.Preferences
import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, UserId)
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
+import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
import Simplex.Messaging.Protocol (SubscriptionMode (..))
import Simplex.Messaging.Util (allFinally)
@@ -389,7 +390,4 @@ createWithRandomBytes size gVar create = tryCreate 3
| otherwise -> throwError . SEInternalError $ show e
encodedRandomBytes :: TVar ChaChaDRG -> Int -> IO ByteString
-encodedRandomBytes gVar = fmap B64.encode . randomBytes gVar
-
-randomBytes :: TVar ChaChaDRG -> Int -> IO ByteString
-randomBytes gVar = atomically . stateTVar gVar . randomBytesGenerate
+encodedRandomBytes gVar n = atomically $ B64.encode <$> C.randomBytes n gVar
diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs
index 4396a900dc..2a0736bc6f 100644
--- a/tests/ChatTests/Files.hs
+++ b/tests/ChatTests/Files.hs
@@ -1094,7 +1094,7 @@ testXFTPFileTransferEncrypted =
let srcPath = "./tests/tmp/alice/test.pdf"
createDirectoryIfMissing True "./tests/tmp/alice/"
createDirectoryIfMissing True "./tests/tmp/bob/"
- WFResult cfArgs <- chatWriteFile srcPath src
+ WFResult cfArgs <- chatWriteFile (chatController alice) srcPath src
let fileJSON = LB.unpack $ J.encode $ CryptoFile srcPath $ Just cfArgs
withXFTPServer $ do
connectUsers alice bob
diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs
index 64fb7c98b8..a6231fa27e 100644
--- a/tests/MobileTests.hs
+++ b/tests/MobileTests.hs
@@ -8,8 +8,8 @@
module MobileTests where
import ChatTests.Utils
+import Control.Concurrent.STM
import Control.Monad.Except
-import Crypto.Random (getRandomBytes)
import Data.Aeson (FromJSON)
import qualified Data.Aeson as J
import qualified Data.Aeson.TH as JQ
@@ -22,8 +22,10 @@ import Data.Word (Word8, Word32)
import Foreign.C
import Foreign.Marshal.Alloc (mallocBytes)
import Foreign.Ptr
+import Foreign.StablePtr
import Foreign.Storable (peek)
import GHC.IO.Encoding (setLocaleEncoding, setFileSystemEncoding, setForeignEncoding)
+import Simplex.Chat.Controller (ChatController (..))
import Simplex.Chat.Mobile
import Simplex.Chat.Mobile.File
import Simplex.Chat.Mobile.Shared
@@ -226,25 +228,29 @@ testChatApi tmp = do
chatParseMarkdown "*hello*" `shouldBe` parsedMarkdown
testMediaApi :: HasCallStack => FilePath -> IO ()
-testMediaApi _ = do
- key :: ByteString <- getRandomBytes 32
- frame <- getRandomBytes 100
+testMediaApi tmp = do
+ Right c@ChatController {random = g} <- chatMigrateInit (tmp > "1") "" "yesUp"
+ cc <- newStablePtr c
+ key <- atomically $ C.randomBytes 32 g
+ frame <- atomically $ C.randomBytes 100 g
let keyStr = strEncode key
reserved = B.replicate (C.authTagSize + C.gcmIVSize) 0
frame' = frame <> reserved
- Right encrypted <- runExceptT $ chatEncryptMedia keyStr frame'
+ Right encrypted <- runExceptT $ chatEncryptMedia cc keyStr frame'
encrypted `shouldNotBe` frame'
B.length encrypted `shouldBe` B.length frame'
runExceptT (chatDecryptMedia keyStr encrypted) `shouldReturn` Right frame'
testMediaCApi :: HasCallStack => FilePath -> IO ()
-testMediaCApi _ = do
- key :: ByteString <- getRandomBytes 32
- frame <- getRandomBytes 100
+testMediaCApi tmp = do
+ Right c@ChatController {random = g} <- chatMigrateInit (tmp > "1") "" "yesUp"
+ cc <- newStablePtr c
+ key <- atomically $ C.randomBytes 32 g
+ frame <- atomically $ C.randomBytes 100 g
let keyStr = strEncode key
reserved = B.replicate (C.authTagSize + C.gcmIVSize) 0
frame' = frame <> reserved
- encrypted <- test cChatEncryptMedia keyStr frame'
+ encrypted <- test (cChatEncryptMedia cc) keyStr frame'
encrypted `shouldNotBe` frame'
test cChatDecryptMedia keyStr encrypted `shouldReturn` frame'
where
@@ -266,6 +272,7 @@ instance FromJSON ReadFileResult where
testFileCApi :: FilePath -> FilePath -> IO ()
testFileCApi fileName tmp = do
+ cc <- mkCCPtr tmp
src <- B.readFile "./tests/fixtures/test.pdf"
let path = tmp > (fileName <> ".pdf")
cPath <- newCString path
@@ -273,7 +280,7 @@ testFileCApi fileName tmp = do
cLen = fromIntegral len
ptr <- mallocBytes $ B.length src
putByteString ptr src
- r <- peekCAString =<< cChatWriteFile cPath ptr cLen
+ r <- peekCAString =<< cChatWriteFile cc cPath ptr cLen
Just (WFResult cfArgs@(CFArgs key nonce)) <- jDecode r
let encryptedFile = CryptoFile path $ Just cfArgs
CF.getFileContentsSize encryptedFile `shouldReturn` fromIntegral (B.length src)
@@ -292,7 +299,7 @@ testMissingFileCApi :: FilePath -> IO ()
testMissingFileCApi tmp = do
let path = tmp > "missing_file"
cPath <- newCString path
- CFArgs key nonce <- CF.randomArgs
+ CFArgs key nonce <- atomically . CF.randomArgs =<< C.newRandom
cKey <- encodedCString key
cNonce <- encodedCString nonce
ptr <- cChatReadFile cPath cKey cNonce
@@ -302,13 +309,14 @@ testMissingFileCApi tmp = do
testFileEncryptionCApi :: FilePath -> FilePath -> IO ()
testFileEncryptionCApi fileName tmp = do
+ cc <- mkCCPtr tmp
let fromPath = tmp > (fileName <> ".source.pdf")
copyFile "./tests/fixtures/test.pdf" fromPath
src <- B.readFile fromPath
cFromPath <- newCString fromPath
let toPath = tmp > (fileName <> ".encrypted.pdf")
cToPath <- newCString toPath
- r <- peekCAString =<< cChatEncryptFile cFromPath cToPath
+ r <- peekCAString =<< cChatEncryptFile cc cFromPath cToPath
Just (WFResult cfArgs@(CFArgs key nonce)) <- jDecode r
CF.getFileContentsSize (CryptoFile toPath $ Just cfArgs) `shouldReturn` fromIntegral (B.length src)
cKey <- encodedCString key
@@ -320,14 +328,15 @@ testFileEncryptionCApi fileName tmp = do
testMissingFileEncryptionCApi :: FilePath -> IO ()
testMissingFileEncryptionCApi tmp = do
+ cc <- mkCCPtr tmp
let fromPath = tmp > "missing_file.source.pdf"
toPath = tmp > "missing_file.encrypted.pdf"
cFromPath <- newCString fromPath
cToPath <- newCString toPath
- r <- peekCAString =<< cChatEncryptFile cFromPath cToPath
+ r <- peekCAString =<< cChatEncryptFile cc cFromPath cToPath
Just (WFError err) <- jDecode r
err `shouldContain` fromPath
- CFArgs key nonce <- CF.randomArgs
+ CFArgs key nonce <- atomically . CF.randomArgs =<< C.newRandom
cKey <- encodedCString key
cNonce <- encodedCString nonce
let toPath' = tmp > "missing_file.decrypted.pdf"
@@ -335,6 +344,9 @@ testMissingFileEncryptionCApi tmp = do
err' <- peekCAString =<< cChatDecryptFile cToPath cKey cNonce cToPath'
err' `shouldContain` toPath
+mkCCPtr :: FilePath -> IO (StablePtr ChatController)
+mkCCPtr tmp = either (error . show) newStablePtr =<< chatMigrateInit (tmp > "1") "" "yesUp"
+
testValidNameCApi :: FilePath -> IO ()
testValidNameCApi _ = do
let goodName = "Джон Доу 👍"
diff --git a/tests/RemoteTests.hs b/tests/RemoteTests.hs
index 13bc2942fc..ff0e5cb2d1 100644
--- a/tests/RemoteTests.hs
+++ b/tests/RemoteTests.hs
@@ -11,18 +11,14 @@ import Control.Logger.Simple
import qualified Data.Aeson as J
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy.Char8 as LB
-import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.Map.Strict as M
-import qualified Network.TLS as TLS
import Simplex.Chat.Archive (archiveFilesFolder)
import Simplex.Chat.Controller (ChatConfig (..), XFTPFileConfig (..), versionNumber)
import qualified Simplex.Chat.Controller as Controller
import Simplex.Chat.Mobile.File
import Simplex.Chat.Remote.Types
-import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFileArgs (..))
import Simplex.Messaging.Encoding.String (strEncode)
-import Simplex.Messaging.Transport.Credentials (genCredentials, tlsCredentials)
import Simplex.Messaging.Util
import System.FilePath ((>))
import Test.Hspec
@@ -571,12 +567,6 @@ contactBob desktop bob = do
(desktop <## "bob (Bob): contact is connected")
(bob <## "alice (Alice): contact is connected")
-genTestCredentials :: IO (C.KeyHash, TLS.Credentials)
-genTestCredentials = do
- caCreds <- liftIO $ genCredentials Nothing (0, 24) "CA"
- sessionCreds <- liftIO $ genCredentials (Just caCreds) (0, 24) "Session"
- pure . tlsCredentials $ sessionCreds :| [caCreds]
-
stopDesktop :: HasCallStack => TestCC -> TestCC -> IO ()
stopDesktop mobile desktop = do
logWarn "stopping via desktop"
diff --git a/tests/Test.hs b/tests/Test.hs
index ee5804aa9a..21aa379c17 100644
--- a/tests/Test.hs
+++ b/tests/Test.hs
@@ -26,7 +26,7 @@ main = do
describe "JSON Tests" jsonTests
describe "SimpleX chat view" viewTests
describe "SimpleX chat protocol" protocolTests
- describe "WebRTC encryption" webRTCTests
+ around tmpBracket $ describe "WebRTC encryption" webRTCTests
describe "Valid names" validNameTests
around testBracket $ do
describe "Mobile API Tests" mobileTests
@@ -35,10 +35,11 @@ main = do
xdescribe'' "SimpleX Directory service bot" directoryServiceTests
describe "Remote session" remoteTests
where
- testBracket test = do
+ testBracket test = withSmpServer $ tmpBracket test
+ tmpBracket test = do
t <- getSystemTime
let ts = show (systemSeconds t) <> show (systemNanoseconds t)
- withSmpServer $ withTmpFiles $ withTempDirectory "tests/tmp" ts test
+ withTmpFiles $ withTempDirectory "tests/tmp" ts test
logCfg :: LogConfig
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
diff --git a/tests/WebRTCTests.hs b/tests/WebRTCTests.hs
index 7dd24e6082..a473afef36 100644
--- a/tests/WebRTCTests.hs
+++ b/tests/WebRTCTests.hs
@@ -1,36 +1,49 @@
+{-# LANGUAGE OverloadedStrings #-}
+
module WebRTCTests where
import Control.Monad.Except
import Crypto.Random (getRandomBytes)
import qualified Data.ByteString.Base64.URL as U
import qualified Data.ByteString.Char8 as B
+import Foreign.StablePtr
+import Simplex.Chat.Mobile
import Simplex.Chat.Mobile.WebRTC
import qualified Simplex.Messaging.Crypto as C
+import System.FilePath ((>))
import Test.Hspec
-webRTCTests :: Spec
+webRTCTests :: SpecWith FilePath
webRTCTests = describe "WebRTC crypto" $ do
- it "encrypts and decrypts media" $ do
+ it "encrypts and decrypts media" $ \tmp -> do
+ Right c <- chatMigrateInit (tmp > "1") "" "yesUp"
+ cc <- newStablePtr c
key <- U.encode <$> getRandomBytes 32
frame <- getRandomBytes 1000
- Right frame' <- runExceptT $ chatEncryptMedia key $ frame <> B.replicate reservedSize '\NUL'
+ Right frame' <- runExceptT $ chatEncryptMedia cc key $ frame <> B.replicate reservedSize '\NUL'
B.length frame' `shouldBe` B.length frame + reservedSize
Right frame'' <- runExceptT $ chatDecryptMedia key frame'
frame'' `shouldBe` frame <> B.replicate reservedSize '\NUL'
- it "should fail on invalid frame size" $ do
+ it "should fail on invalid frame size" $ \tmp -> do
+ Right c <- chatMigrateInit (tmp > "1") "" "yesUp"
+ cc <- newStablePtr c
key <- U.encode <$> getRandomBytes 32
frame <- getRandomBytes 10
- runExceptT (chatEncryptMedia key frame) `shouldReturn` Left "frame has no [reserved space for] IV and/or auth tag"
+ runExceptT (chatEncryptMedia cc key frame) `shouldReturn` Left "frame has no [reserved space for] IV and/or auth tag"
runExceptT (chatDecryptMedia key frame) `shouldReturn` Left "frame has no [reserved space for] IV and/or auth tag"
- it "should fail on invalid key" $ do
+ it "should fail on invalid key" $ \tmp -> do
+ Right c <- chatMigrateInit (tmp > "1") "" "yesUp"
+ cc <- newStablePtr c
let key = B.replicate 32 '#'
frame <- (<> B.replicate reservedSize '\NUL') <$> getRandomBytes 100
- runExceptT (chatEncryptMedia key frame) `shouldReturn` Left "invalid key: invalid character at offset: 0"
+ runExceptT (chatEncryptMedia cc key frame) `shouldReturn` Left "invalid key: invalid character at offset: 0"
runExceptT (chatDecryptMedia key frame) `shouldReturn` Left "invalid key: invalid character at offset: 0"
- it "should fail on invalid auth tag" $ do
+ it "should fail on invalid auth tag" $ \tmp -> do
+ Right c <- chatMigrateInit (tmp > "1") "" "yesUp"
+ cc <- newStablePtr c
key <- U.encode <$> getRandomBytes 32
frame <- getRandomBytes 1000
- Right frame' <- runExceptT $ chatEncryptMedia key $ frame <> B.replicate reservedSize '\NUL'
+ Right frame' <- runExceptT $ chatEncryptMedia cc key $ frame <> B.replicate reservedSize '\NUL'
Right frame'' <- runExceptT $ chatDecryptMedia key frame'
frame'' `shouldBe` frame <> B.replicate reservedSize '\NUL'
let (encFrame, rest) = B.splitAt (B.length frame' - reservedSize) frame
From d198d6a8db1ccb72d2db04826ad3803c3d148d36 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Thu, 21 Dec 2023 09:57:43 +0000
Subject: [PATCH 08/32] core: build iOS library with ghc 9.6.3 with iPhone7
etc. support (#3577)
* bump haskell.nix
* bump flake.lock
* Try openssl fix
* CFLAGS. not CCFLAGS
* Fix iOS build issues and improve static library handling
---------
Co-authored-by: Moritz Angermann
---
flake.lock | 36 +++++++++++++++++++++++++++---------
flake.nix | 13 ++++++++++++-
2 files changed, 39 insertions(+), 10 deletions(-)
diff --git a/flake.lock b/flake.lock
index e5f8d531cc..a11e01683e 100644
--- a/flake.lock
+++ b/flake.lock
@@ -119,12 +119,15 @@
}
},
"flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
"locked": {
- "lastModified": 1676283394,
- "narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
+ "lastModified": 1701680307,
+ "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
- "rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
+ "rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
@@ -190,11 +193,11 @@
"hackage": {
"flake": false,
"locked": {
- "lastModified": 1702340598,
- "narHash": "sha256-CC0HI+6iKPtH+8r/ZfcpW5v/OYvL7zMwpr0xfkXV1zU=",
+ "lastModified": 1702513363,
+ "narHash": "sha256-kloro9uEe8aYhPMoMjVNq2rfrXNgMOZhOPwVH5DH2K0=",
"owner": "input-output-hk",
"repo": "hackage.nix",
- "rev": "24617c569995e38bf3b83b48eec6628a50fdb4fb",
+ "rev": "a9d931d0398da67846fa257922a924829233cb91",
"type": "github"
},
"original": {
@@ -240,11 +243,11 @@
"stackage": "stackage"
},
"locked": {
- "lastModified": 1700119633,
- "narHash": "sha256-nZY2eIo8TkRbXgJXEWMm9zor330GuUtcNzvUN9tN64U=",
+ "lastModified": 1701163700,
+ "narHash": "sha256-sOrewUS3LnzV09nGr7+3R6Q6zsgU4smJc61QsHq+4DE=",
"owner": "input-output-hk",
"repo": "haskell.nix",
- "rev": "1fe47a3d52e1ecd6247c8ab83811a21de2e2f074",
+ "rev": "2808bfe3e62e9eb4ee8974cd623a00e1611f302b",
"type": "github"
},
"original": {
@@ -673,6 +676,21 @@
"repo": "stackage.nix",
"type": "github"
}
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
}
},
"root": "root",
diff --git a/flake.nix b/flake.nix
index 999f097c24..6fabe7d657 100644
--- a/flake.nix
+++ b/flake.nix
@@ -67,6 +67,9 @@
}); in
let iosPostInstall = bundleName: ''
${pkgs.tree}/bin/tree $out
+ mkdir tmp
+ find ./dist -name "libHS*-ghc*.a" -exec cp {} tmp \;
+ (cd tmp; ${pkgs.tree}/bin/tree .; ar x libHS*.a; for o in *.o; do if /usr/bin/otool -xv $o|grep ldadd ; then echo $o; fi; done; cd ..; rm -fR tmp)
mkdir -p $out/_pkg
# copy over includes, we might want those, but maybe not.
# cp -r $out/lib/*/*/include $out/_pkg/
@@ -82,6 +85,13 @@
${mac2ios.packages.${system}.mac2ios}/bin/mac2ios $pkg
chmod -w $pkg
done
+
+ mkdir tmp
+ find $out/_pkg -name "libHS*-ghc*.a" -exec cp {} tmp \;
+ (cd tmp; ${pkgs.tree}/bin/tree .; ar x libHS*.a; for o in *.o; do if /usr/bin/otool -xv $o|grep ldadd ; then echo $o; fi; done; cd ..; rm -fR tmp)
+
+ sha256sum $out/_pkg/*.a
+
(cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/${bundleName}.zip *)
rm -fR $out/_pkg
mkdir -p $out/nix-support
@@ -536,7 +546,8 @@
packages.direct-sqlcipher.flags.commoncrypto = true;
packages.entropy.flags.DoNotGetEntropy = true;
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
- (pkgs.openssl.override { static = true; })
+ # TODO: have a cross override for iOS, that sets this.
+ ((pkgs.openssl.override { static = true; }).overrideDerivation (old: { CFLAGS = "-mcpu=apple-a7 -march=armv8-a+norcpc" ;}))
];
}];
}).simplex-chat.components.library.override (
From aa037c0662d10fd5f35f5a632e5bb12cce82440f Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Thu, 21 Dec 2023 10:05:43 +0000
Subject: [PATCH 09/32] ios: update core library (uses GHC 9.6.3)
---
apps/ios/SimpleX.xcodeproj/project.pbxproj | 40 +++++++++++-----------
1 file changed, 20 insertions(+), 20 deletions(-)
diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj
index f5701af155..4302b6fb80 100644
--- a/apps/ios/SimpleX.xcodeproj/project.pbxproj
+++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj
@@ -116,11 +116,11 @@
5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; };
5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */; };
5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; };
- 5CCD1A882B2A5D56001A4199 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A832B2A5D55001A4199 /* libgmp.a */; };
- 5CCD1A892B2A5D56001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A842B2A5D55001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn.a */; };
- 5CCD1A8A2B2A5D56001A4199 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A852B2A5D55001A4199 /* libgmpxx.a */; };
- 5CCD1A8B2B2A5D56001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A862B2A5D55001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn-ghc8.10.7.a */; };
- 5CCD1A8C2B2A5D56001A4199 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A872B2A5D56001A4199 /* libffi.a */; };
+ 5CCD1B0A2B3444B9001A4199 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1B052B3444B8001A4199 /* libffi.a */; };
+ 5CCD1B0B2B3444B9001A4199 /* libHSsimplex-chat-5.4.0.7-K3rb8mQtqiP3LyZDoNKwwg-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1B062B3444B8001A4199 /* libHSsimplex-chat-5.4.0.7-K3rb8mQtqiP3LyZDoNKwwg-ghc9.6.3.a */; };
+ 5CCD1B0C2B3444B9001A4199 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1B072B3444B8001A4199 /* libgmpxx.a */; };
+ 5CCD1B0D2B3444B9001A4199 /* libHSsimplex-chat-5.4.0.7-K3rb8mQtqiP3LyZDoNKwwg.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1B082B3444B9001A4199 /* libHSsimplex-chat-5.4.0.7-K3rb8mQtqiP3LyZDoNKwwg.a */; };
+ 5CCD1B0E2B3444B9001A4199 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1B092B3444B9001A4199 /* libgmp.a */; };
5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.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, ); }; };
@@ -402,11 +402,11 @@
5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; };
5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIRcvDecryptionError.swift; sourceTree = ""; };
5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = ""; };
- 5CCD1A832B2A5D55001A4199 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; };
- 5CCD1A842B2A5D55001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn.a"; sourceTree = ""; };
- 5CCD1A852B2A5D55001A4199 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; };
- 5CCD1A862B2A5D55001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn-ghc8.10.7.a"; sourceTree = ""; };
- 5CCD1A872B2A5D56001A4199 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; };
+ 5CCD1B052B3444B8001A4199 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; };
+ 5CCD1B062B3444B8001A4199 /* libHSsimplex-chat-5.4.0.7-K3rb8mQtqiP3LyZDoNKwwg-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.7-K3rb8mQtqiP3LyZDoNKwwg-ghc9.6.3.a"; sourceTree = ""; };
+ 5CCD1B072B3444B8001A4199 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; };
+ 5CCD1B082B3444B9001A4199 /* libHSsimplex-chat-5.4.0.7-K3rb8mQtqiP3LyZDoNKwwg.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.7-K3rb8mQtqiP3LyZDoNKwwg.a"; sourceTree = ""; };
+ 5CCD1B092B3444B9001A4199 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; };
5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = ""; };
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 = ""; };
@@ -518,12 +518,12 @@
buildActionMask = 2147483647;
files = (
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
- 5CCD1A8B2B2A5D56001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn-ghc8.10.7.a in Frameworks */,
- 5CCD1A8A2B2A5D56001A4199 /* libgmpxx.a in Frameworks */,
- 5CCD1A882B2A5D56001A4199 /* libgmp.a in Frameworks */,
- 5CCD1A8C2B2A5D56001A4199 /* libffi.a in Frameworks */,
- 5CCD1A892B2A5D56001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn.a in Frameworks */,
+ 5CCD1B0E2B3444B9001A4199 /* libgmp.a in Frameworks */,
+ 5CCD1B0C2B3444B9001A4199 /* libgmpxx.a in Frameworks */,
+ 5CCD1B0B2B3444B9001A4199 /* libHSsimplex-chat-5.4.0.7-K3rb8mQtqiP3LyZDoNKwwg-ghc9.6.3.a in Frameworks */,
+ 5CCD1B0A2B3444B9001A4199 /* libffi.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
+ 5CCD1B0D2B3444B9001A4199 /* libHSsimplex-chat-5.4.0.7-K3rb8mQtqiP3LyZDoNKwwg.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -585,11 +585,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
- 5CCD1A872B2A5D56001A4199 /* libffi.a */,
- 5CCD1A832B2A5D55001A4199 /* libgmp.a */,
- 5CCD1A852B2A5D55001A4199 /* libgmpxx.a */,
- 5CCD1A862B2A5D55001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn-ghc8.10.7.a */,
- 5CCD1A842B2A5D55001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn.a */,
+ 5CCD1B052B3444B8001A4199 /* libffi.a */,
+ 5CCD1B092B3444B9001A4199 /* libgmp.a */,
+ 5CCD1B072B3444B8001A4199 /* libgmpxx.a */,
+ 5CCD1B062B3444B8001A4199 /* libHSsimplex-chat-5.4.0.7-K3rb8mQtqiP3LyZDoNKwwg-ghc9.6.3.a */,
+ 5CCD1B082B3444B9001A4199 /* libHSsimplex-chat-5.4.0.7-K3rb8mQtqiP3LyZDoNKwwg.a */,
);
path = Libraries;
sourceTree = "";
From 2bff3b9c97c76b0ae66a81fb4aeb59147e143d38 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Thu, 21 Dec 2023 12:49:18 +0000
Subject: [PATCH 10/32] desktop, android: update api to pass controller when
encrypting files (use ChaChaDRG as source of randomness) (#3578)
---
.../common/src/commonMain/cpp/android/simplex-api.c | 12 ++++++------
.../common/src/commonMain/cpp/desktop/simplex-api.c | 12 ++++++------
.../kotlin/chat/simplex/common/model/CryptoFile.kt | 6 ++++--
.../kotlin/chat/simplex/common/platform/Core.kt | 4 ++--
4 files changed, 18 insertions(+), 16 deletions(-)
diff --git a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c
index 4fd62524de..676c58fb49 100644
--- a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c
+++ b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c
@@ -65,9 +65,9 @@ extern char *chat_parse_markdown(const char *str);
extern char *chat_parse_server(const char *str);
extern char *chat_password_hash(const char *pwd, const char *salt);
extern char *chat_valid_name(const char *name);
-extern char *chat_write_file(const char *path, char *ptr, int length);
+extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
extern char *chat_read_file(const char *path, const char *key, const char *nonce);
-extern char *chat_encrypt_file(const char *from_path, const char *to_path);
+extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path);
extern char *chat_decrypt_file(const char *from_path, const char *key, const char *nonce, const char *to_path);
JNIEXPORT jobjectArray JNICALL
@@ -157,11 +157,11 @@ Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz
}
JNIEXPORT jstring JNICALL
-Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jstring path, jobject buffer) {
+Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) {
const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE);
jbyte *buff = (jbyte *) (*env)->GetDirectBufferAddress(env, buffer);
jlong capacity = (*env)->GetDirectBufferCapacity(env, buffer);
- jstring res = (*env)->NewStringUTF(env, chat_write_file(_path, buff, capacity));
+ jstring res = (*env)->NewStringUTF(env, chat_write_file((void*)controller, _path, buff, capacity));
(*env)->ReleaseStringUTFChars(env, path, _path);
return res;
}
@@ -206,10 +206,10 @@ Java_chat_simplex_common_platform_CoreKt_chatReadFile(JNIEnv *env, jclass clazz,
}
JNIEXPORT jstring JNICALL
-Java_chat_simplex_common_platform_CoreKt_chatEncryptFile(JNIEnv *env, jclass clazz, jstring from_path, jstring to_path) {
+Java_chat_simplex_common_platform_CoreKt_chatEncryptFile(JNIEnv *env, jclass clazz, jlong controller, jstring from_path, jstring to_path) {
const char *_from_path = (*env)->GetStringUTFChars(env, from_path, JNI_FALSE);
const char *_to_path = (*env)->GetStringUTFChars(env, to_path, JNI_FALSE);
- jstring res = (*env)->NewStringUTF(env, chat_encrypt_file(_from_path, _to_path));
+ jstring res = (*env)->NewStringUTF(env, chat_encrypt_file((void*)controller, _from_path, _to_path));
(*env)->ReleaseStringUTFChars(env, from_path, _from_path);
(*env)->ReleaseStringUTFChars(env, to_path, _to_path);
return res;
diff --git a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c
index fb561dc38d..292715bdc5 100644
--- a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c
+++ b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c
@@ -38,9 +38,9 @@ extern char *chat_parse_markdown(const char *str);
extern char *chat_parse_server(const char *str);
extern char *chat_password_hash(const char *pwd, const char *salt);
extern char *chat_valid_name(const char *name);
-extern char *chat_write_file(const char *path, char *ptr, int length);
+extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
extern char *chat_read_file(const char *path, const char *key, const char *nonce);
-extern char *chat_encrypt_file(const char *from_path, const char *to_path);
+extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path);
extern char *chat_decrypt_file(const char *from_path, const char *key, const char *nonce, const char *to_path);
// As a reference: https://stackoverflow.com/a/60002045
@@ -167,11 +167,11 @@ Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz
}
JNIEXPORT jstring JNICALL
-Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jstring path, jobject buffer) {
+Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) {
const char *_path = encode_to_utf8_chars(env, path);
jbyte *buff = (jbyte *) (*env)->GetDirectBufferAddress(env, buffer);
jlong capacity = (*env)->GetDirectBufferCapacity(env, buffer);
- jstring res = decode_to_utf8_string(env, chat_write_file(_path, buff, capacity));
+ jstring res = decode_to_utf8_string(env, chat_write_file((void*)controller, _path, buff, capacity));
(*env)->ReleaseStringUTFChars(env, path, _path);
return res;
}
@@ -216,10 +216,10 @@ Java_chat_simplex_common_platform_CoreKt_chatReadFile(JNIEnv *env, jclass clazz,
}
JNIEXPORT jstring JNICALL
-Java_chat_simplex_common_platform_CoreKt_chatEncryptFile(JNIEnv *env, jclass clazz, jstring from_path, jstring to_path) {
+Java_chat_simplex_common_platform_CoreKt_chatEncryptFile(JNIEnv *env, jclass clazz, jlong controller, jstring from_path, jstring to_path) {
const char *_from_path = encode_to_utf8_chars(env, from_path);
const char *_to_path = encode_to_utf8_chars(env, to_path);
- jstring res = decode_to_utf8_string(env, chat_encrypt_file(_from_path, _to_path));
+ jstring res = decode_to_utf8_string(env, chat_encrypt_file((void*)controller, _from_path, _to_path));
(*env)->ReleaseStringUTFChars(env, from_path, _from_path);
(*env)->ReleaseStringUTFChars(env, to_path, _to_path);
return res;
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt
index 037d27af33..28b46f592d 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt
@@ -21,10 +21,11 @@ sealed class WriteFileResult {
* */
fun writeCryptoFile(path: String, data: ByteArray): CryptoFileArgs {
+ val ctrl = ChatController.ctrl ?: throw Exception("Controller is not initialized")
val buffer = ByteBuffer.allocateDirect(data.size)
buffer.put(data)
buffer.rewind()
- val str = chatWriteFile(path, buffer)
+ val str = chatWriteFile(ctrl, path, buffer)
return when (val d = json.decodeFromString(WriteFileResult.serializer(), str)) {
is WriteFileResult.Result -> d.cryptoArgs
is WriteFileResult.Error -> throw Exception(d.writeError)
@@ -43,7 +44,8 @@ fun readCryptoFile(path: String, cryptoArgs: CryptoFileArgs): ByteArray {
}
fun encryptCryptoFile(fromPath: String, toPath: String): CryptoFileArgs {
- val str = chatEncryptFile(fromPath, toPath)
+ val ctrl = ChatController.ctrl ?: throw Exception("Controller is not initialized")
+ val str = chatEncryptFile(ctrl, fromPath, toPath)
val d = json.decodeFromString(WriteFileResult.serializer(), str)
return when (d) {
is WriteFileResult.Result -> d.cryptoArgs
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt
index a4c1c333e5..7d097efb7a 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt
@@ -22,9 +22,9 @@ external fun chatParseMarkdown(str: String): String
external fun chatParseServer(str: String): String
external fun chatPasswordHash(pwd: String, salt: String): String
external fun chatValidName(name: String): String
-external fun chatWriteFile(path: String, buffer: ByteBuffer): String
+external fun chatWriteFile(ctrl: ChatCtrl, path: String, buffer: ByteBuffer): String
external fun chatReadFile(path: String, key: String, nonce: String): Array
-external fun chatEncryptFile(fromPath: String, toPath: String): String
+external fun chatEncryptFile(ctrl: ChatCtrl, fromPath: String, toPath: String): String
external fun chatDecryptFile(fromPath: String, key: String, nonce: String, toPath: String): String
val chatModel: ChatModel
From c4855313b63fd010dc674d1b51ef1be0f89d13f9 Mon Sep 17 00:00:00 2001
From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
Date: Thu, 21 Dec 2023 21:49:49 +0800
Subject: [PATCH 11/32] android: splash screen with background color on Android
12+ (#3579)
---
.../android/src/main/AndroidManifest.xml | 1 +
.../java/chat/simplex/app/MainActivity.kt | 2 ++
.../main/java/chat/simplex/app/SimplexApp.kt | 28 +++++++++++++++----
.../src/main/res/values-night/themes.xml | 8 ++++++
.../android/src/main/res/values/themes.xml | 2 +-
.../chat/simplex/common/platform/Platform.kt | 1 +
.../simplex/common/ui/theme/ThemeManager.kt | 2 ++
7 files changed, 38 insertions(+), 6 deletions(-)
create mode 100644 apps/multiplatform/android/src/main/res/values-night/themes.xml
diff --git a/apps/multiplatform/android/src/main/AndroidManifest.xml b/apps/multiplatform/android/src/main/AndroidManifest.xml
index 09b33316a4..d8350ee222 100644
--- a/apps/multiplatform/android/src/main/AndroidManifest.xml
+++ b/apps/multiplatform/android/src/main/AndroidManifest.xml
@@ -39,6 +39,7 @@
android:exported="true"
android:label="${app_name}"
android:windowSoftInputMode="adjustResize"
+ android:configChanges="uiMode"
android:theme="@style/Theme.SimpleX">
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 082c10582c..8d64ae3c80 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
@@ -5,6 +5,7 @@ import android.net.Uri
import android.os.*
import android.view.WindowManager
import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.FragmentActivity
import chat.simplex.app.model.NtfManager
import chat.simplex.app.model.NtfManager.getUserIdFromIntent
@@ -22,6 +23,7 @@ import java.lang.ref.WeakReference
class MainActivity: FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
+ platform.androidSetNightModeIfSupported()
applyAppLocale(ChatModel.controller.appPrefs.appLanguage)
super.onCreate(savedInstanceState)
// testJson()
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 e3f4e69bd4..ee43da5d44 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
@@ -1,9 +1,8 @@
package chat.simplex.app
import android.app.Application
-import android.os.Handler
-import android.os.Looper
-import chat.simplex.common.platform.Log
+import android.app.UiModeManager
+import android.os.*
import androidx.lifecycle.*
import androidx.work.*
import chat.simplex.app.model.NtfManager
@@ -12,10 +11,12 @@ import chat.simplex.common.helpers.requiresIgnoringBattery
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.updatingChatsMutex
+import chat.simplex.common.platform.*
+import chat.simplex.common.ui.theme.CurrentColors
+import chat.simplex.common.ui.theme.DefaultTheme
+import chat.simplex.common.views.call.RcvCallInvitation
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage
-import chat.simplex.common.platform.*
-import chat.simplex.common.views.call.RcvCallInvitation
import com.jakewharton.processphoenix.ProcessPhoenix
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.withLock
@@ -225,6 +226,23 @@ class SimplexApp: Application(), LifecycleEventObserver {
override fun androidIsBackgroundCallAllowed(): Boolean = !SimplexService.isBackgroundRestricted()
+ override fun androidSetNightModeIfSupported() {
+ if (Build.VERSION.SDK_INT < 31) return
+
+ val light = if (CurrentColors.value.name == DefaultTheme.SYSTEM.name) {
+ null
+ } else {
+ CurrentColors.value.colors.isLight
+ }
+ val mode = when (light) {
+ null -> UiModeManager.MODE_NIGHT_AUTO
+ true -> UiModeManager.MODE_NIGHT_NO
+ false -> UiModeManager.MODE_NIGHT_YES
+ }
+ val uiModeManager = androidAppContext.getSystemService(UI_MODE_SERVICE) as UiModeManager
+ uiModeManager.setApplicationNightMode(mode)
+ }
+
override suspend fun androidAskToAllowBackgroundCalls(): Boolean {
if (SimplexService.isBackgroundRestricted()) {
val userChoice: CompletableDeferred = CompletableDeferred()
diff --git a/apps/multiplatform/android/src/main/res/values-night/themes.xml b/apps/multiplatform/android/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000000..bb341b71d0
--- /dev/null
+++ b/apps/multiplatform/android/src/main/res/values-night/themes.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/apps/multiplatform/android/src/main/res/values/themes.xml b/apps/multiplatform/android/src/main/res/values/themes.xml
index f59d099fba..eb6d85bf05 100644
--- a/apps/multiplatform/android/src/main/res/values/themes.xml
+++ b/apps/multiplatform/android/src/main/res/values/themes.xml
@@ -1,7 +1,7 @@
-
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt
index 84ffdb6fd7..e55c2c939a 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt
@@ -10,6 +10,7 @@ interface PlatformInterface {
fun androidChatStopped() {}
fun androidChatInitializedAndStarted() {}
fun androidIsBackgroundCallAllowed(): Boolean = true
+ fun androidSetNightModeIfSupported() {}
suspend fun androidAskToAllowBackgroundCalls(): Boolean = true
}
/**
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt
index 4a7521efb8..49d3203455 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt
@@ -7,6 +7,7 @@ import androidx.compose.ui.text.font.FontFamily
import chat.simplex.res.MR
import chat.simplex.common.model.AppPreferences
import chat.simplex.common.model.ChatController
+import chat.simplex.common.platform.platform
import chat.simplex.common.views.helpers.generalGetString
// https://github.com/rsms/inter
@@ -96,6 +97,7 @@ object ThemeManager {
fun applyTheme(theme: String, darkForSystemTheme: Boolean) {
appPrefs.currentTheme.set(theme)
CurrentColors.value = currentColors(darkForSystemTheme)
+ platform.androidSetNightModeIfSupported()
}
fun changeDarkTheme(theme: String, darkForSystemTheme: Boolean) {
From 8b0d2dede7d81cd2041d60ab55b1d856a40bd859 Mon Sep 17 00:00:00 2001
From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
Date: Thu, 21 Dec 2023 23:19:36 +0800
Subject: [PATCH 12/32] android, desktop: saving and sharing files menu item
(#3580)
---
.../simplex/common/views/chat/item/ChatItemView.kt | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt
index abb67da50a..daf887e8c3 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt
@@ -195,7 +195,13 @@ fun ChatItemView(
}
val clipboard = LocalClipboardManager.current
val cachedRemoteReqs = remember { CIFile.cachedRemoteFileRequests }
- val copyAndShareAllowed = cItem.file == null || !chatModel.connectedToRemote() || getLoadedFilePath(cItem.file) != null || cachedRemoteReqs[cItem.file.fileSource] != false
+ val copyAndShareAllowed = when {
+ cItem.content.text.isNotEmpty() -> true
+ cItem.file != null && chatModel.connectedToRemote() && cachedRemoteReqs[cItem.file.fileSource] != false && cItem.file.loaded -> true
+ getLoadedFilePath(cItem.file) != null -> true
+ else -> false
+ }
+
if (copyAndShareAllowed) {
ItemAction(stringResource(MR.strings.share_verb), painterResource(MR.images.ic_share), onClick = {
var fileSource = getLoadedFileSource(cItem.file)
@@ -221,7 +227,7 @@ fun ChatItemView(
showMenu.value = false
})
}
- if ((cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCVideo || cItem.content.msgContent is MsgContent.MCFile || cItem.content.msgContent is MsgContent.MCVoice) && (getLoadedFilePath(cItem.file) != null || (chatModel.connectedToRemote() && cachedRemoteReqs[cItem.file?.fileSource] != false))) {
+ if ((cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCVideo || cItem.content.msgContent is MsgContent.MCFile || cItem.content.msgContent is MsgContent.MCVoice) && (getLoadedFilePath(cItem.file) != null || (chatModel.connectedToRemote() && cachedRemoteReqs[cItem.file?.fileSource] != false && cItem.file?.loaded == true))) {
SaveContentItemAction(cItem, saveFileLauncher, showMenu)
}
if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice && !live) {
From c83238c35a9101f3a6c9b2630962ebd22d0d40ad Mon Sep 17 00:00:00 2001
From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
Date: Fri, 22 Dec 2023 06:48:32 +0800
Subject: [PATCH 13/32] desktop: enable sending images and files with enter
(#3582)
---
.../common/platform/PlatformTextField.android.kt | 2 +-
.../simplex/common/platform/PlatformTextField.kt | 2 +-
.../chat/simplex/common/views/chat/SendMsgView.kt | 13 ++++++-------
.../common/views/database/DatabaseErrorView.kt | 2 +-
.../views/onboarding/SetupDatabasePassphrase.kt | 4 ++--
.../common/platform/PlatformTextField.desktop.kt | 5 +++--
6 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt
index 1bc9658496..9e28c4f2bc 100644
--- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt
+++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt
@@ -27,7 +27,6 @@ import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.inputmethod.InputConnectionCompat
import androidx.core.widget.doAfterTextChanged
import androidx.core.widget.doOnTextChanged
-import chat.simplex.common.*
import chat.simplex.common.R
import chat.simplex.common.helpers.toURI
import chat.simplex.common.model.ChatModel
@@ -45,6 +44,7 @@ import java.net.URI
actual fun PlatformTextField(
composeState: MutableState,
sendMsgEnabled: Boolean,
+ sendMsgButtonDisabled: Boolean,
textStyle: MutableState,
showDeleteTextButton: MutableState,
userIsObserver: Boolean,
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt
index fa99d0f93c..af47f9c3e0 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt
@@ -4,13 +4,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.text.TextStyle
import chat.simplex.common.views.chat.ComposeState
-import java.io.File
import java.net.URI
@Composable
expect fun PlatformTextField(
composeState: MutableState,
sendMsgEnabled: Boolean,
+ sendMsgButtonDisabled: Boolean,
textStyle: MutableState,
showDeleteTextButton: MutableState,
userIsObserver: Boolean,
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt
index 28882e6b73..e566cf30d3 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt
@@ -29,7 +29,6 @@ import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource
import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.*
-import java.io.File
import java.net.URI
@Composable
@@ -82,7 +81,10 @@ fun SendMsgView(
val showVoiceButton = !nextSendGrpInv && cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started)
val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
- PlatformTextField(composeState, sendMsgEnabled, textStyle, showDeleteTextButton, userIsObserver, onMessageChange, editPrevMessage, onFilesPasted) {
+ val sendMsgButtonDisabled = !sendMsgEnabled || !cs.sendEnabled() ||
+ (!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) ||
+ cs.endLiveDisabled
+ PlatformTextField(composeState, sendMsgEnabled, sendMsgButtonDisabled, textStyle, showDeleteTextButton, userIsObserver, onMessageChange, editPrevMessage, onFilesPasted) {
if (!cs.inProgress) {
sendMessage(null)
}
@@ -155,9 +157,6 @@ fun SendMsgView(
else -> {
val cs = composeState.value
val icon = if (cs.editing || cs.liveMessage != null) painterResource(MR.images.ic_check_filled) else painterResource(MR.images.ic_arrow_upward)
- val disabled = !sendMsgEnabled || !cs.sendEnabled() ||
- (!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) ||
- cs.endLiveDisabled
val showDropdown = rememberSaveable { mutableStateOf(false) }
@Composable
@@ -200,12 +199,12 @@ fun SendMsgView(
val menuItems = MenuItems()
if (menuItems.isNotEmpty()) {
- SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !disabled, sendMessage) { showDropdown.value = true }
+ SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendMessage) { showDropdown.value = true }
DefaultDropdownMenu(showDropdown) {
menuItems.forEach { composable -> composable() }
}
} else {
- SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !disabled, sendMessage)
+ SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendMessage)
}
}
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt
index 4e5424215b..22d69de1cc 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt
@@ -270,7 +270,7 @@ private fun DatabaseKeyField(text: MutableState, enabled: Boolean, onCli
} else null
),
modifier = Modifier.focusRequester(focusRequester).onPreviewKeyEvent {
- if (onClick != null && it.key == Key.Enter && it.type == KeyEventType.KeyUp) {
+ if (onClick != null && (it.key == Key.Enter || it.key == Key.NumPadEnter) && it.type == KeyEventType.KeyUp) {
onClick()
true
} else {
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 c117e89971..a51d9c8a0c 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
@@ -120,7 +120,7 @@ private fun SetupDatabasePassphraseLayout(
.padding(horizontal = DEFAULT_PADDING)
.focusRequester(focusRequester)
.onPreviewKeyEvent {
- if (it.key == Key.Enter && it.type == KeyEventType.KeyUp) {
+ if ((it.key == Key.Enter || it.key == Key.NumPadEnter) && it.type == KeyEventType.KeyUp) {
focusManager.moveFocus(FocusDirection.Down)
true
} else {
@@ -150,7 +150,7 @@ private fun SetupDatabasePassphraseLayout(
modifier = Modifier
.padding(horizontal = DEFAULT_PADDING)
.onPreviewKeyEvent {
- if (!disabled && it.key == Key.Enter && it.type == KeyEventType.KeyUp) {
+ if (!disabled && (it.key == Key.Enter || it.key == Key.NumPadEnter) && it.type == KeyEventType.KeyUp) {
onClickUpdate()
true
} else {
diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt
index 74df6b8251..8016b18b12 100644
--- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt
+++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt
@@ -45,6 +45,7 @@ import kotlin.text.substring
actual fun PlatformTextField(
composeState: MutableState,
sendMsgEnabled: Boolean,
+ sendMsgButtonDisabled: Boolean,
textStyle: MutableState,
showDeleteTextButton: MutableState,
userIsObserver: Boolean,
@@ -103,7 +104,7 @@ actual fun PlatformTextField(
.padding(vertical = 4.dp)
.focusRequester(focusRequester)
.onPreviewKeyEvent {
- if (it.key == Key.Enter && it.type == KeyEventType.KeyDown) {
+ if ((it.key == Key.Enter || it.key == Key.NumPadEnter) && it.type == KeyEventType.KeyDown) {
if (it.isShiftPressed) {
val start = if (minOf(textFieldValue.selection.min) == 0) "" else textFieldValue.text.substring(0 until textFieldValue.selection.min)
val newText = start + "\n" +
@@ -113,7 +114,7 @@ actual fun PlatformTextField(
selection = TextRange(textFieldValue.selection.min + 1)
)
onMessageChange(newText)
- } else if (cs.message.isNotEmpty()) {
+ } else if (!sendMsgButtonDisabled) {
onDone()
}
true
From 67590f3258d8b72e1fdd6fb31da6cd8d6bb6780d Mon Sep 17 00:00:00 2001
From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
Date: Fri, 22 Dec 2023 16:46:55 +0800
Subject: [PATCH 14/32] Revert "ios: making thumbnails faster" (#3571)
This reverts commit cd9cb8e064b3df1902c5c2321556b0ca1dc5e9b8.
---
apps/ios/Shared/Views/Helpers/ImagePicker.swift | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/ios/Shared/Views/Helpers/ImagePicker.swift b/apps/ios/Shared/Views/Helpers/ImagePicker.swift
index 0e3f8082b3..efd42ee4bd 100644
--- a/apps/ios/Shared/Views/Helpers/ImagePicker.swift
+++ b/apps/ios/Shared/Views/Helpers/ImagePicker.swift
@@ -143,7 +143,7 @@ struct LibraryMediaListPicker: UIViewControllerRepresentable {
config.filter = .any(of: [.images, .videos])
config.selectionLimit = selectionLimit
config.selection = .ordered
- config.preferredAssetRepresentationMode = .current
+ //config.preferredAssetRepresentationMode = .current
let controller = PHPickerViewController(configuration: config)
controller.delegate = context.coordinator
return controller
From 57a6e85668fc4b461a4881b86c58ae40afb0e3c6 Mon Sep 17 00:00:00 2001
From: Andor Kesselman
Date: Fri, 22 Dec 2023 00:47:48 -0800
Subject: [PATCH 15/32] docs: fix typo (#3552)
---
docs/protocol/simplex-chat.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/protocol/simplex-chat.md b/docs/protocol/simplex-chat.md
index 95069c794c..71d5efcef7 100644
--- a/docs/protocol/simplex-chat.md
+++ b/docs/protocol/simplex-chat.md
@@ -173,7 +173,7 @@ This message is used to delete previously sent chat items. Receiving clients MUS
When content message `x.msg.new` contains file attachment (the invitation to receive the file), this sub-protocol is used to accept this file or to notify the recipient that sending the file was cancelled.
-File attachement can optionally include connection address to receive the file - clients MUST include it when sending files to direct connections, and MUST NOT include it when sending file attachment to the group (as different members would need different connections to receive the file).
+File attachment can optionally include connection address to receive the file - clients MUST include it when sending files to direct connections, and MUST NOT include it when sending file attachment to the group (as different members would need different connections to receive the file).
`x.file.acpt` message is used to accept the file in case when file connection address was included in the message (that is the case when the file invitation was sent in direct message). It is sent as part of file connection handshake via file connection, that is why this message contains no reference to the file - the used connection provides sufficient context for the sender.
From 23989aca571b04e401435a08d7a251e5a538b189 Mon Sep 17 00:00:00 2001
From: Andor Kesselman
Date: Fri, 22 Dec 2023 00:48:26 -0800
Subject: [PATCH 16/32] Update README.md (#3553)
---
apps/simplex-chat/README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/simplex-chat/README.md b/apps/simplex-chat/README.md
index 113f90d185..bbcf40b139 100644
--- a/apps/simplex-chat/README.md
+++ b/apps/simplex-chat/README.md
@@ -1,3 +1,3 @@
# SimpleX Chat CLI app
-See [repo REAMDE](../../README.md#zap-quick-installation-of-a-terminal-app) for installation and usage instructions.
+See [repo README](../../README.md#zap-quick-installation-of-a-terminal-app) for installation and usage instructions.
From f93f68e425f4b27302a91129cc9eb5a06a51be0c Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Sat, 23 Dec 2023 13:06:59 +0000
Subject: [PATCH 17/32] core: agent background mode for iOS NSE (#3574)
* core: agent background mode for iOS NSE
* change parameter for APIActivateChat
* fix
* update lib
* update lib
* simplexmq
* simplify
---
apps/ios/Shared/SimpleXApp.swift | 6 +-
.../ios/SimpleX NSE/NotificationService.swift | 2 +-
apps/ios/SimpleXChat/API.swift | 4 +-
apps/ios/SimpleXChat/SimpleX.h | 2 +-
cabal.project | 2 +-
scripts/nix/sha256map.nix | 2 +-
src/Simplex/Chat.hs | 151 +++++++++---------
src/Simplex/Chat/Core.hs | 2 +-
src/Simplex/Chat/Mobile.hs | 18 +--
tests/ChatClient.hs | 2 +-
10 files changed, 98 insertions(+), 93 deletions(-)
diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift
index c023f375d3..f72ffcaaaf 100644
--- a/apps/ios/Shared/SimpleXApp.swift
+++ b/apps/ios/Shared/SimpleXApp.swift
@@ -21,10 +21,10 @@ struct SimpleXApp: App {
@State private var enteredBackgroundAuthenticated: TimeInterval? = nil
init() {
-// DispatchQueue.global(qos: .background).sync {
- haskell_init()
+ DispatchQueue.global(qos: .background).sync {
+ haskell_init()
// hs_init(0, nil)
-// }
+ }
UserDefaults.standard.register(defaults: appDefaults)
setGroupDefaults()
registerGroupDefaults()
diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift
index c286ee1c3c..f9b4852e53 100644
--- a/apps/ios/SimpleX NSE/NotificationService.swift
+++ b/apps/ios/SimpleX NSE/NotificationService.swift
@@ -442,7 +442,7 @@ func startChat() -> DBMigrationResult? {
func doStartChat() -> DBMigrationResult? {
logger.debug("NotificationService: doStartChat")
hs_init(0, nil)
- let (_, dbStatus) = chatMigrateInit(confirmMigrations: defaultMigrationConfirmation())
+ let (_, dbStatus) = chatMigrateInit(confirmMigrations: defaultMigrationConfirmation(), backgroundMode: true)
if dbStatus != .ok {
resetChatCtrl()
NSEChatState.shared.set(.created)
diff --git a/apps/ios/SimpleXChat/API.swift b/apps/ios/SimpleXChat/API.swift
index dfa4caf099..8d05a066e8 100644
--- a/apps/ios/SimpleXChat/API.swift
+++ b/apps/ios/SimpleXChat/API.swift
@@ -17,7 +17,7 @@ public func getChatCtrl(_ useKey: String? = nil) -> chat_ctrl {
fatalError("chat controller not initialized")
}
-public func chatMigrateInit(_ useKey: String? = nil, confirmMigrations: MigrationConfirmation? = nil) -> (Bool, DBMigrationResult) {
+public func chatMigrateInit(_ useKey: String? = nil, confirmMigrations: MigrationConfirmation? = nil, backgroundMode: Bool = false) -> (Bool, DBMigrationResult) {
if let res = migrationResult { return res }
let dbPath = getAppDatabasePath().path
var dbKey = ""
@@ -41,7 +41,7 @@ public func chatMigrateInit(_ useKey: String? = nil, confirmMigrations: Migratio
var cKey = dbKey.cString(using: .utf8)!
var cConfirm = confirm.rawValue.cString(using: .utf8)!
// the last parameter of chat_migrate_init is used to return the pointer to chat controller
- let cjson = chat_migrate_init_key(&cPath, &cKey, 1, &cConfirm, &chatController)!
+ let cjson = chat_migrate_init_key(&cPath, &cKey, 1, &cConfirm, backgroundMode ? 1 : 0, &chatController)!
let dbRes = dbMigrationResult(fromCString(cjson))
let encrypted = dbKey != ""
let keychainErr = dbRes == .ok && useKeychain && encrypted && !kcDatabasePassword.set(dbKey)
diff --git a/apps/ios/SimpleXChat/SimpleX.h b/apps/ios/SimpleXChat/SimpleX.h
index 909d76a76c..c49d104514 100644
--- a/apps/ios/SimpleXChat/SimpleX.h
+++ b/apps/ios/SimpleXChat/SimpleX.h
@@ -16,7 +16,7 @@ extern void hs_init(int argc, char **argv[]);
typedef void* chat_ctrl;
// the last parameter is used to return the pointer to chat controller
-extern char *chat_migrate_init_key(char *path, char *key, int keepKey, char *confirm, chat_ctrl *ctrl);
+extern char *chat_migrate_init_key(char *path, char *key, int keepKey, char *confirm, int backgroundMode, chat_ctrl *ctrl);
extern char *chat_close_store(chat_ctrl ctl);
extern char *chat_reopen_store(chat_ctrl ctl);
extern char *chat_send_cmd(chat_ctrl ctl, char *cmd);
diff --git a/cabal.project b/cabal.project
index 1ff8aacd77..ca967b458d 100644
--- a/cabal.project
+++ b/cabal.project
@@ -14,7 +14,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
- tag: 13a60d1d3944aa175311563e661161e759b92563
+ tag: 9ea9b2c7356a9b42be8ab685c343076ff3c452fe
source-repository-package
type: git
diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix
index 595d40c4e7..626d2d8515 100644
--- a/scripts/nix/sha256map.nix
+++ b/scripts/nix/sha256map.nix
@@ -1,5 +1,5 @@
{
- "https://github.com/simplex-chat/simplexmq.git"."13a60d1d3944aa175311563e661161e759b92563" = "08mvqrbjfnq7c6mhkj4hhy4cxn0cj21n49lqzh67ani71g2g1xwa";
+ "https://github.com/simplex-chat/simplexmq.git"."9ea9b2c7356a9b42be8ab685c343076ff3c452fe" = "16jgsh5wnf8q56hlsdpa5xf2qhlrv80j8088xys0sbwfa4br2nk8";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 8bce204f54..61e32cb3d0 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -197,79 +197,84 @@ createChatDatabase filePrefix key keepKey confirmMigrations = runExceptT $ do
agentStore <- ExceptT $ createAgentStore (agentStoreFile filePrefix) key keepKey confirmMigrations
pure ChatDatabase {chatStore, agentStore}
-newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> IO ChatController
-newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, tempDir, deviceNameForRemote} ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, networkConfig, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable}, deviceName, optFilesFolder, showReactions, allowInstantFiles, autoAcceptFileSize} = do
- let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False}
- config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable}
- firstTime = dbNew chatStore
- currentUser <- newTVarIO user
- currentRemoteHost <- newTVarIO Nothing
- servers <- agentServers config
- smpAgent <- getSMPAgentClient aCfg {tbqSize} servers agentStore
- agentAsync <- newTVarIO Nothing
- random <- liftIO C.newRandom
- inputQ <- newTBQueueIO tbqSize
- outputQ <- newTBQueueIO tbqSize
- connNetworkStatuses <- atomically TM.empty
- subscriptionMode <- newTVarIO SMSubscribe
- chatLock <- newEmptyTMVarIO
- sndFiles <- newTVarIO M.empty
- rcvFiles <- newTVarIO M.empty
- currentCalls <- atomically TM.empty
- localDeviceName <- newTVarIO $ fromMaybe deviceNameForRemote deviceName
- multicastSubscribers <- newTMVarIO 0
- remoteSessionSeq <- newTVarIO 0
- remoteHostSessions <- atomically TM.empty
- remoteHostsFolder <- newTVarIO Nothing
- remoteCtrlSession <- newTVarIO Nothing
- filesFolder <- newTVarIO optFilesFolder
- chatStoreChanged <- newTVarIO False
- expireCIThreads <- newTVarIO M.empty
- expireCIFlags <- newTVarIO M.empty
- cleanupManagerAsync <- newTVarIO Nothing
- timedItemThreads <- atomically TM.empty
- showLiveItems <- newTVarIO False
- encryptLocalFiles <- newTVarIO False
- userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg
- tempDirectory <- newTVarIO tempDir
- contactMergeEnabled <- newTVarIO True
- pure
- ChatController
- { firstTime,
- currentUser,
- currentRemoteHost,
- smpAgent,
- agentAsync,
- chatStore,
- chatStoreChanged,
- random,
- inputQ,
- outputQ,
- connNetworkStatuses,
- subscriptionMode,
- chatLock,
- sndFiles,
- rcvFiles,
- currentCalls,
- localDeviceName,
- multicastSubscribers,
- remoteSessionSeq,
- remoteHostSessions,
- remoteHostsFolder,
- remoteCtrlSession,
- config,
- filesFolder,
- expireCIThreads,
- expireCIFlags,
- cleanupManagerAsync,
- timedItemThreads,
- showLiveItems,
- encryptLocalFiles,
- userXFTPFileConfig,
- tempDirectory,
- logFilePath = logFile,
- contactMergeEnabled
- }
+newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> Bool -> IO ChatController
+newChatController
+ ChatDatabase {chatStore, agentStore}
+ user
+ cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, tempDir, deviceNameForRemote}
+ ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, networkConfig, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable}, deviceName, optFilesFolder, showReactions, allowInstantFiles, autoAcceptFileSize}
+ backgroundMode = do
+ let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False}
+ config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable}
+ firstTime = dbNew chatStore
+ currentUser <- newTVarIO user
+ currentRemoteHost <- newTVarIO Nothing
+ servers <- agentServers config
+ smpAgent <- getSMPAgentClient aCfg {tbqSize} servers agentStore backgroundMode
+ agentAsync <- newTVarIO Nothing
+ random <- liftIO C.newRandom
+ inputQ <- newTBQueueIO tbqSize
+ outputQ <- newTBQueueIO tbqSize
+ connNetworkStatuses <- atomically TM.empty
+ subscriptionMode <- newTVarIO SMSubscribe
+ chatLock <- newEmptyTMVarIO
+ sndFiles <- newTVarIO M.empty
+ rcvFiles <- newTVarIO M.empty
+ currentCalls <- atomically TM.empty
+ localDeviceName <- newTVarIO $ fromMaybe deviceNameForRemote deviceName
+ multicastSubscribers <- newTMVarIO 0
+ remoteSessionSeq <- newTVarIO 0
+ remoteHostSessions <- atomically TM.empty
+ remoteHostsFolder <- newTVarIO Nothing
+ remoteCtrlSession <- newTVarIO Nothing
+ filesFolder <- newTVarIO optFilesFolder
+ chatStoreChanged <- newTVarIO False
+ expireCIThreads <- newTVarIO M.empty
+ expireCIFlags <- newTVarIO M.empty
+ cleanupManagerAsync <- newTVarIO Nothing
+ timedItemThreads <- atomically TM.empty
+ showLiveItems <- newTVarIO False
+ encryptLocalFiles <- newTVarIO False
+ userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg
+ tempDirectory <- newTVarIO tempDir
+ contactMergeEnabled <- newTVarIO True
+ pure
+ ChatController
+ { firstTime,
+ currentUser,
+ currentRemoteHost,
+ smpAgent,
+ agentAsync,
+ chatStore,
+ chatStoreChanged,
+ random,
+ inputQ,
+ outputQ,
+ connNetworkStatuses,
+ subscriptionMode,
+ chatLock,
+ sndFiles,
+ rcvFiles,
+ currentCalls,
+ localDeviceName,
+ multicastSubscribers,
+ remoteSessionSeq,
+ remoteHostSessions,
+ remoteHostsFolder,
+ remoteCtrlSession,
+ config,
+ filesFolder,
+ expireCIThreads,
+ expireCIFlags,
+ cleanupManagerAsync,
+ timedItemThreads,
+ showLiveItems,
+ encryptLocalFiles,
+ userXFTPFileConfig,
+ tempDirectory,
+ logFilePath = logFile,
+ contactMergeEnabled
+ }
where
configServers :: DefaultAgentServers
configServers =
diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs
index c409526a0b..1d870bf381 100644
--- a/src/Simplex/Chat/Core.hs
+++ b/src/Simplex/Chat/Core.hs
@@ -28,7 +28,7 @@ simplexChatCore cfg@ChatConfig {confirmMigrations, testView} opts@ChatOpts {core
exitFailure
run db@ChatDatabase {chatStore} = do
u <- getCreateActiveUser chatStore testView
- cc <- newChatController db (Just u) cfg opts
+ cc <- newChatController db (Just u) cfg opts False
runSimplexChat opts u cc chat
runSimplexChat :: ChatOpts -> User -> ChatController -> (User -> ChatController -> IO ()) -> IO ()
diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs
index 6540352a3d..d7f2e5a43c 100644
--- a/src/Simplex/Chat/Mobile.hs
+++ b/src/Simplex/Chat/Mobile.hs
@@ -72,7 +72,7 @@ $(JQ.deriveToJSON defaultJSON ''APIResponse)
foreign export ccall "chat_migrate_init" cChatMigrateInit :: CString -> CString -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString
-foreign export ccall "chat_migrate_init_key" cChatMigrateInitKey :: CString -> CString -> CInt -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString
+foreign export ccall "chat_migrate_init_key" cChatMigrateInitKey :: CString -> CString -> CInt -> CString -> CInt -> Ptr (StablePtr ChatController) -> IO CJSONString
foreign export ccall "chat_close_store" cChatCloseStore :: StablePtr ChatController -> IO CString
@@ -108,10 +108,10 @@ foreign export ccall "chat_decrypt_file" cChatDecryptFile :: CString -> CString
-- | check / migrate database and initialize chat controller on success
cChatMigrateInit :: CString -> CString -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString
-cChatMigrateInit fp key = cChatMigrateInitKey fp key 0
+cChatMigrateInit fp key conf = cChatMigrateInitKey fp key 0 conf 0
-cChatMigrateInitKey :: CString -> CString -> CInt -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString
-cChatMigrateInitKey fp key keepKey conf ctrl = do
+cChatMigrateInitKey :: CString -> CString -> CInt -> CString -> CInt -> Ptr (StablePtr ChatController) -> IO CJSONString
+cChatMigrateInitKey fp key keepKey conf background ctrl = do
-- ensure we are set to UTF-8; iOS does not have locale, and will default to
-- US-ASCII all the time.
setLocaleEncoding utf8
@@ -122,7 +122,7 @@ cChatMigrateInitKey fp key keepKey conf ctrl = do
dbKey <- BA.convert <$> B.packCString key
confirm <- peekCAString conf
r <-
- chatMigrateInitKey dbPath dbKey (keepKey /= 0) confirm >>= \case
+ chatMigrateInitKey dbPath dbKey (keepKey /= 0) confirm (background /= 0) >>= \case
Right cc -> (newStablePtr cc >>= poke ctrl) $> DBMOk
Left e -> pure e
newCStringFromLazyBS $ J.encode r
@@ -220,10 +220,10 @@ getActiveUser_ :: SQLiteStore -> IO (Maybe User)
getActiveUser_ st = find activeUser <$> withTransaction st getUsers
chatMigrateInit :: String -> ScrubbedBytes -> String -> IO (Either DBMigrationResult ChatController)
-chatMigrateInit dbFilePrefix dbKey = chatMigrateInitKey dbFilePrefix dbKey False
+chatMigrateInit dbFilePrefix dbKey confirm = chatMigrateInitKey dbFilePrefix dbKey False confirm False
-chatMigrateInitKey :: String -> ScrubbedBytes -> Bool -> String -> IO (Either DBMigrationResult ChatController)
-chatMigrateInitKey dbFilePrefix dbKey keepKey confirm = runExceptT $ do
+chatMigrateInitKey :: String -> ScrubbedBytes -> Bool -> String -> Bool -> IO (Either DBMigrationResult ChatController)
+chatMigrateInitKey dbFilePrefix dbKey keepKey confirm backgroundMode = runExceptT $ do
confirmMigrations <- liftEitherWith (const DBMInvalidConfirmation) $ strDecode $ B.pack confirm
chatStore <- migrate createChatStore (chatStoreFile dbFilePrefix) confirmMigrations
agentStore <- migrate createAgentStore (agentStoreFile dbFilePrefix) confirmMigrations
@@ -231,7 +231,7 @@ chatMigrateInitKey dbFilePrefix dbKey keepKey confirm = runExceptT $ do
where
initialize st db = do
user_ <- getActiveUser_ st
- newChatController db user_ defaultMobileConfig (mobileChatOpts dbFilePrefix)
+ newChatController db user_ defaultMobileConfig (mobileChatOpts dbFilePrefix) backgroundMode
migrate createStore dbFile confirmMigrations =
ExceptT $
(first (DBMErrorMigration dbFile) <$> createStore dbFile dbKey keepKey confirmMigrations)
diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs
index c32d8002b9..044d95eaaf 100644
--- a/tests/ChatClient.hs
+++ b/tests/ChatClient.hs
@@ -175,7 +175,7 @@ startTestChat_ :: ChatDatabase -> ChatConfig -> ChatOpts -> User -> IO TestCC
startTestChat_ db cfg opts user = do
t <- withVirtualTerminal termSettings pure
ct <- newChatTerminal t opts
- cc <- newChatController db (Just user) cfg opts
+ cc <- newChatController db (Just user) cfg opts False
chatAsync <- async . runSimplexChat opts user cc $ \_u cc' -> runChatTerminal ct cc' opts
atomically . unless (maintenance opts) $ readTVar (agentAsync cc) >>= \a -> when (isNothing a) retry
termQ <- newTQueueIO
From 12d1ada25ea972989a53c732b0b0e6baedd5595b Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Sat, 23 Dec 2023 17:07:23 +0400
Subject: [PATCH 18/32] core: support batch sending in groups, batch
introductions; send recent message history to new members (#3519)
* core: batch send stubs, comments
* multiple events in ChatMessage and supporting types
* Revert "multiple events in ChatMessage and supporting types"
This reverts commit 9b239b26ba5c8fdec41c6689a6421baf7ffcc27d.
* schema, refactor group processing for batched messages
* encoding, refactor processing
* refactor code to work with updated schema
* encoding, remove instances
* wip
* implement batching
* batch introductions
* wip
* collect and send message history
* missing new line
* rename
* test
* rework to build history via chat items
* refactor, tests
* correctly set member version range, dont include deleted items
* tests
* fix disappearing messages
* check number of errors
* comment
* check size in encodeChatMessage
* fix - don't check msg size for binary
* use builder
* rename
* rename
* rework batching
* lazy msg body
* use withStoreBatch
* refactor
* reverse batches
* comment
* possibly fix builder for single msg
* refactor batcher
* refactor
* dont repopulate msg_deliveries on down migration
* EncodedChatMessage type
* remove type
* batcher tests
* add tests
* group history preference
* test group link
* fix tests
* fix for random update
* add test testImageFitsSingleBatch
* refactor
* rename function
* refactor
* mconcat
* rename feature
* catch error on each batch
* refactor file inv retrieval
* refactor gathering item forward events
* refactor message batching
* unite migrations
* move files
* refactor
* Revert "unite migrations"
This reverts commit 0be7a3117a2b4eb7f13f1ff639188bb3ff826af8.
* refactor splitFileDescr
* improve tests
* Revert "dont repopulate msg_deliveries on down migration"
This reverts commit 2944c1cc28acf85282a85d8458c67cefb7787ac7.
* fix down migration
---------
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
---
simplex-chat.cabal | 3 +
src/Simplex/Chat.hs | 449 +++++++----
src/Simplex/Chat/Help.hs | 3 +-
src/Simplex/Chat/Messages.hs | 19 +-
src/Simplex/Chat/Messages/Batch.hs | 53 ++
src/Simplex/Chat/Messages/CIContent.hs | 10 +-
.../M20231215_recreate_msg_deliveries.hs | 100 +++
src/Simplex/Chat/Migrations/chat_schema.sql | 48 +-
src/Simplex/Chat/Protocol.hs | 58 +-
src/Simplex/Chat/Store/Files.hs | 45 +-
src/Simplex/Chat/Store/Groups.hs | 2 +-
src/Simplex/Chat/Store/Messages.hs | 158 ++--
src/Simplex/Chat/Store/Migrations.hs | 4 +-
src/Simplex/Chat/Store/Shared.hs | 14 +-
src/Simplex/Chat/Types.hs | 7 +-
src/Simplex/Chat/Types/Preferences.hs | 41 +-
src/Simplex/Chat/Util.hs | 14 +-
tests/ChatTests/Files.hs | 8 +-
tests/ChatTests/Groups.hs | 746 ++++++++++++++++++
tests/ChatTests/Profiles.hs | 9 +-
tests/ChatTests/Utils.hs | 10 +-
tests/MessageBatching.hs | 120 +++
tests/ProtocolTests.hs | 32 +-
tests/SchemaDump.hs | 4 +-
tests/Test.hs | 2 +
25 files changed, 1616 insertions(+), 343 deletions(-)
create mode 100644 src/Simplex/Chat/Messages/Batch.hs
create mode 100644 src/Simplex/Chat/Migrations/M20231215_recreate_msg_deliveries.hs
create mode 100644 tests/MessageBatching.hs
diff --git a/simplex-chat.cabal b/simplex-chat.cabal
index 6462d26008..64ab4954fe 100644
--- a/simplex-chat.cabal
+++ b/simplex-chat.cabal
@@ -36,6 +36,7 @@ library
Simplex.Chat.Help
Simplex.Chat.Markdown
Simplex.Chat.Messages
+ Simplex.Chat.Messages.Batch
Simplex.Chat.Messages.CIContent
Simplex.Chat.Messages.CIContent.Events
Simplex.Chat.Migrations.M20220101_initial
@@ -127,6 +128,7 @@ library
Simplex.Chat.Migrations.M20231126_remote_ctrl_address
Simplex.Chat.Migrations.M20231207_chat_list_pagination
Simplex.Chat.Migrations.M20231214_item_content_tag
+ Simplex.Chat.Migrations.M20231215_recreate_msg_deliveries
Simplex.Chat.Mobile
Simplex.Chat.Mobile.File
Simplex.Chat.Mobile.Shared
@@ -543,6 +545,7 @@ test-suite simplex-chat-test
ChatTests.Utils
JSONTests
MarkdownTests
+ MessageBatching
MobileTests
ProtocolTests
RemoteTests
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 61e32cb3d0..f5803eb626 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -29,6 +29,7 @@ import Data.Bifunctor (bimap, first)
import Data.ByteArray (ScrubbedBytes)
import qualified Data.ByteArray as BA
import qualified Data.ByteString.Base64 as B64
+import Data.ByteString.Builder (toLazyByteString)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
@@ -38,20 +39,19 @@ import Data.Either (fromRight, lefts, partitionEithers, rights)
import Data.Fixed (div')
import Data.Functor (($>))
import Data.Int (Int64)
-import Data.List (find, foldl', isSuffixOf, partition, sortBy, sortOn)
-import Data.List.NonEmpty (NonEmpty, nonEmpty)
+import Data.List (find, foldl', isSuffixOf, partition, sortOn)
+import Data.List.NonEmpty (NonEmpty (..), nonEmpty, toList, (<|))
import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe, maybeToList)
-import Data.Ord (comparing)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
import Data.Time (NominalDiffTime, addUTCTime, defaultTimeLocale, formatTime)
import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDay, nominalDiffTimeToSeconds)
import Data.Time.Clock.System (systemToUTCTime)
-import Data.Word (Word16, Word32)
+import Data.Word (Word32)
import qualified Database.SQLite.Simple as SQL
import Simplex.Chat.Archive
import Simplex.Chat.Call
@@ -59,6 +59,7 @@ import Simplex.Chat.Controller
import Simplex.Chat.Files
import Simplex.Chat.Markdown
import Simplex.Chat.Messages
+import Simplex.Chat.Messages.Batch (MsgBatch (..), batchMessages)
import Simplex.Chat.Messages.CIContent
import Simplex.Chat.Messages.CIContent.Events
import Simplex.Chat.Options
@@ -77,7 +78,7 @@ import Simplex.Chat.Store.Shared
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Util
-import Simplex.Chat.Util (encryptFile)
+import Simplex.Chat.Util (encryptFile, shuffle)
import Simplex.FileTransfer.Client.Main (maxFileSize)
import Simplex.FileTransfer.Client.Presets (defaultXFTPServers)
import Simplex.FileTransfer.Description (ValidFileDescription, gb, kb, mb)
@@ -607,7 +608,7 @@ processChatCommand = \case
<$> withConnection st (readTVarIO . DB.slow)
APIGetChats {userId, pendingConnections, pagination, query} -> withUserId' userId $ \user -> do
(errs, previews) <- partitionEithers <$> withStore' (\db -> getChatPreviews db user pendingConnections pagination query)
- toView $ CRChatErrors (Just user) (map ChatErrorStore errs)
+ unless (null errs) $ toView $ CRChatErrors (Just user) (map ChatErrorStore errs)
pure $ CRApiChats user previews
APIGetChat (ChatRef cType cId) pagination search -> withUser $ \user -> case cType of
-- TODO optimize queries calculating ChatStats, currently they're disabled
@@ -688,7 +689,7 @@ processChatCommand = \case
withStore $ \db -> getDirectChatItem db user chatId quotedItemId
(origQmc, qd, sent) <- quoteData qci
let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Nothing}
- qmc = quoteContent origQmc file
+ qmc = quoteContent mc origQmc file
quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc, formattedText}
pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Just quotedItem)
where
@@ -702,13 +703,13 @@ processChatCommand = \case
assertUserGroupRole gInfo GRAuthor
send g
where
- send g@(Group gInfo@GroupInfo {groupId, membership} ms)
+ send g@(Group gInfo@GroupInfo {groupId} ms)
| isVoice mc && not (groupFeatureAllowed SGFVoice gInfo) = notAllowedError GFVoice
| not (isVoice mc) && isJust file_ && not (groupFeatureAllowed SGFFiles gInfo) = notAllowedError GFFiles
| otherwise = do
(fInv_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer g (length $ filter memberCurrent ms)
timed_ <- sndGroupCITimed live gInfo itemTTL
- (msgContainer, quotedItem_) <- prepareMsg fInv_ timed_ membership
+ (msgContainer, quotedItem_) <- prepareGroupMsg user gInfo mc quotedItemId_ fInv_ timed_ live
(msg@SndMessage {sharedMsgId}, sentToMembers) <- sendGroupMessage user gInfo ms (XMsgNew msgContainer)
ci <- saveSndChatItem' user (CDGroupSnd gInfo) msg (CISndMsgContent mc) ciFile_ quotedItem_ timed_ live
withStore' $ \db ->
@@ -748,51 +749,9 @@ processChatCommand = \case
void . withStore' $ \db -> createSndGroupInlineFT db m conn ft
sendMemberFileInline m conn ft sharedMsgId
processMember _ = pure ()
- prepareMsg :: Maybe FileInvitation -> Maybe CITimed -> GroupMember -> m (MsgContainer, Maybe (CIQuote 'CTGroup))
- prepareMsg fInv_ timed_ membership = case quotedItemId_ of
- Nothing -> pure (MCSimple (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing)
- Just quotedItemId -> do
- CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <-
- withStore $ \db -> getGroupChatItem db user chatId quotedItemId
- (origQmc, qd, sent, GroupMember {memberId}) <- quoteData qci membership
- let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Just memberId}
- qmc = quoteContent origQmc file
- quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc, formattedText}
- pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Just quotedItem)
- where
- quoteData :: ChatItem c d -> GroupMember -> m (MsgContent, CIQDirection 'CTGroup, Bool, GroupMember)
- quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} _ = throwChatError CEInvalidQuote
- quoteData ChatItem {chatDir = CIGroupSnd, content = CISndMsgContent qmc} membership' = pure (qmc, CIQGroupSnd, True, membership')
- quoteData ChatItem {chatDir = CIGroupRcv m, content = CIRcvMsgContent qmc} _ = pure (qmc, CIQGroupRcv $ Just m, False, m)
- quoteData _ _ = throwChatError CEInvalidQuote
CTContactRequest -> pure $ chatCmdError (Just user) "not supported"
CTContactConnection -> pure $ chatCmdError (Just user) "not supported"
where
- quoteContent :: forall d. MsgContent -> Maybe (CIFile d) -> MsgContent
- quoteContent qmc ciFile_
- | replaceContent = MCText qTextOrFile
- | otherwise = case qmc of
- MCImage _ image -> MCImage qTextOrFile image
- MCFile _ -> MCFile qTextOrFile
- -- consider same for voice messages
- -- MCVoice _ voice -> MCVoice qTextOrFile voice
- _ -> qmc
- where
- -- if the message we're quoting with is one of the "large" MsgContents
- -- we replace the quote's content with MCText
- replaceContent = case mc of
- MCText _ -> False
- MCFile _ -> False
- MCLink {} -> True
- MCImage {} -> True
- MCVideo {} -> True
- MCVoice {} -> False
- MCUnknown {} -> True
- qText = msgContentText qmc
- getFileName :: CIFile d -> String
- getFileName CIFile {fileName} = fileName
- qFileName = maybe qText (T.pack . getFileName) ciFile_
- qTextOrFile = if T.null qText then qFileName else qText
xftpSndFileTransfer :: User -> CryptoFile -> Integer -> Int -> ContactOrGroup -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta)
xftpSndFileTransfer user file@(CryptoFile filePath cfArgs) fileSize n contactOrGroup = do
let fileName = takeFileName filePath
@@ -1836,7 +1795,7 @@ processChatCommand = \case
LastChats count_ -> withUser' $ \user -> do
let count = fromMaybe 5000 count_
(errs, previews) <- partitionEithers <$> withStore' (\db -> getChatPreviews db user False (PTLast count) clqNoFilters)
- toView $ CRChatErrors (Just user) (map ChatErrorStore errs)
+ unless (null errs) $ toView $ CRChatErrors (Just user) (map ChatErrorStore errs)
pure $ CRChats previews
LastMessages (Just chatName) count search -> withUser $ \user -> do
chatRef <- getChatRef user chatName
@@ -2307,7 +2266,7 @@ processChatCommand = \case
tryChatError (withStore (`getUser` userId)) >>= \case
Left _ -> throwChatError CEUserUnknown
Right user -> pure user
- validateUserPassword :: User -> User -> Maybe UserPwd -> m ()
+ validateUserPassword :: User -> User -> Maybe UserPwd -> m ()
validateUserPassword = validateUserPassword_ . Just
validateUserPassword_ :: Maybe User -> User -> Maybe UserPwd -> m ()
validateUserPassword_ user_ User {userId = userId', viewPwdHash} viewPwd_ =
@@ -2433,6 +2392,50 @@ processChatCommand = \case
cReqHashes = bimap hash hash cReqSchemas
hash = ConnReqUriHash . C.sha256Hash . strEncode
+prepareGroupMsg :: forall m. ChatMonad m => User -> GroupInfo -> MsgContent -> Maybe ChatItemId -> Maybe FileInvitation -> Maybe CITimed -> Bool -> m (MsgContainer, Maybe (CIQuote 'CTGroup))
+prepareGroupMsg user GroupInfo {groupId, membership} mc quotedItemId_ fInv_ timed_ live = case quotedItemId_ of
+ Nothing -> pure (MCSimple (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing)
+ Just quotedItemId -> do
+ CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <-
+ withStore $ \db -> getGroupChatItem db user groupId quotedItemId
+ (origQmc, qd, sent, GroupMember {memberId}) <- quoteData qci membership
+ let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Just memberId}
+ qmc = quoteContent mc origQmc file
+ quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc, formattedText}
+ pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Just quotedItem)
+ where
+ quoteData :: ChatItem c d -> GroupMember -> m (MsgContent, CIQDirection 'CTGroup, Bool, GroupMember)
+ quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} _ = throwChatError CEInvalidQuote
+ quoteData ChatItem {chatDir = CIGroupSnd, content = CISndMsgContent qmc} membership' = pure (qmc, CIQGroupSnd, True, membership')
+ quoteData ChatItem {chatDir = CIGroupRcv m, content = CIRcvMsgContent qmc} _ = pure (qmc, CIQGroupRcv $ Just m, False, m)
+ quoteData _ _ = throwChatError CEInvalidQuote
+
+quoteContent :: forall d. MsgContent -> MsgContent -> Maybe (CIFile d) -> MsgContent
+quoteContent mc qmc ciFile_
+ | replaceContent = MCText qTextOrFile
+ | otherwise = case qmc of
+ MCImage _ image -> MCImage qTextOrFile image
+ MCFile _ -> MCFile qTextOrFile
+ -- consider same for voice messages
+ -- MCVoice _ voice -> MCVoice qTextOrFile voice
+ _ -> qmc
+ where
+ -- if the message we're quoting with is one of the "large" MsgContents
+ -- we replace the quote's content with MCText
+ replaceContent = case mc of
+ MCText _ -> False
+ MCFile _ -> False
+ MCLink {} -> True
+ MCImage {} -> True
+ MCVideo {} -> True
+ MCVoice {} -> False
+ MCUnknown {} -> True
+ qText = msgContentText qmc
+ getFileName :: CIFile d -> String
+ getFileName CIFile {fileName} = fileName
+ qFileName = maybe qText (T.pack . getFileName) ciFile_
+ qTextOrFile = if T.null qText then qFileName else qText
+
assertDirectAllowed :: ChatMonad m => User -> MsgDirection -> Contact -> CMEventTag e -> m ()
assertDirectAllowed user dir ct event =
unless (allowedChatEvent || anyDirectOrUsed ct) . unlessM directMessagesAllowed $
@@ -2610,7 +2613,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI
-- marking file as accepted and reading description in the same transaction
-- to prevent race condition with appending description
ci <- xftpAcceptRcvFT db user fileId filePath
- rfd <- getRcvFileDescrByFileId db fileId
+ rfd <- getRcvFileDescrByRcvFileId db fileId
pure (ci, rfd)
receiveViaCompleteFD user fileId rfd cryptoArgs
pure ci
@@ -3188,17 +3191,29 @@ processAgentMsgSndFile _corrId aFileId msg =
sendFileDescription sft rfd msgId sendMsg = do
let rfdText = fileDescrText rfd
withStore' $ \db -> updateSndFTDescrXFTP db user sft rfdText
- partSize <- asks $ xftpDescrPartSize . config
- sendParts 1 partSize rfdText
+ parts <- splitFileDescr rfdText
+ loopSend parts
where
- sendParts partNo partSize rfdText = do
- let (part, rest) = T.splitAt partSize rfdText
- complete = T.null rest
- fileDescr = FileDescr {fileDescrText = part, fileDescrPartNo = partNo, fileDescrComplete = complete}
+ -- returns msgDeliveryId of the last file description message
+ loopSend :: NonEmpty FileDescr -> m Int64
+ loopSend (fileDescr :| fds) = do
(_, msgDeliveryId) <- sendMsg $ XMsgFileDescr {msgId, fileDescr}
- if complete
- then pure msgDeliveryId
- else sendParts (partNo + 1) partSize rest
+ case L.nonEmpty fds of
+ Just fds' -> loopSend fds'
+ Nothing -> pure msgDeliveryId
+
+splitFileDescr :: ChatMonad m => RcvFileDescrText -> m (NonEmpty FileDescr)
+splitFileDescr rfdText = do
+ partSize <- asks $ xftpDescrPartSize . config
+ pure $ splitParts 1 partSize rfdText
+ where
+ splitParts partNo partSize remText =
+ let (part, rest) = T.splitAt partSize remText
+ complete = T.null rest
+ fileDescr = FileDescr {fileDescrText = part, fileDescrPartNo = partNo, fileDescrComplete = complete}
+ in if complete
+ then fileDescr :| []
+ else fileDescr <| splitParts (partNo + 1) partSize rest
processAgentMsgRcvFile :: forall m. ChatMonad m => ACorrId -> RcvFileId -> ACommand 'Agent 'AERcvFile -> m ()
processAgentMsgRcvFile _corrId aFileId msg =
@@ -3293,6 +3308,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
pure ()
MSG meta _msgFlags msgBody -> do
cmdId <- createAckCmd conn
+ -- TODO only acknowledge without saving message?
+ -- probably this branch is never executed, so there should be no reason
+ -- to save message if contact hasn't been created yet - chat item isn't created anyway
withAckMessage agentConnId cmdId meta $ do
(_conn', _) <- saveDirectRcvMSG conn meta cmdId msgBody
pure False
@@ -3568,21 +3586,105 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
let Connection {viaUserContactLink} = conn
when (isJust viaUserContactLink && isNothing (memberContactId m)) sendXGrpLinkMem
members <- withStore' $ \db -> getGroupMembers db user gInfo
- intros <- withStore' $ \db -> createIntroductions db members m
void . sendGroupMessage user gInfo members . XGrpMemNew $ memberInfo m
- shuffledIntros <- liftIO $ shuffleMembers intros $ \GroupMemberIntro {reMember = GroupMember {memberRole}} -> memberRole
- forM_ shuffledIntros $ \intro ->
- processIntro intro `catchChatError` (toView . CRChatError (Just user))
+ sendIntroductions members
+ when (groupFeatureAllowed SGFHistory gInfo) sendHistory
where
sendXGrpLinkMem = do
let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo
profileToSend = profileToSendOnAccept user profileMode
void $ sendDirectMessage conn (XGrpLinkMem profileToSend) (GroupId groupId)
+ sendIntroductions members = do
+ intros <- withStore' $ \db -> createIntroductions db members m
+ shuffledIntros <- liftIO $ shuffleIntros intros
+ if isCompatibleRange (memberChatVRange' m) batchSendVRange
+ then do
+ let events = map (XGrpMemIntro . memberInfo . reMember) shuffledIntros
+ forM_ (L.nonEmpty events) $ \events' ->
+ sendGroupMemberMessages user conn events' groupId
+ else forM_ shuffledIntros $ \intro ->
+ processIntro intro `catchChatError` (toView . CRChatError (Just user))
+ shuffleIntros :: [GroupMemberIntro] -> IO [GroupMemberIntro]
+ shuffleIntros intros = do
+ let (admins, others) = partition isAdmin intros
+ (admPics, admNoPics) = partition hasPicture admins
+ (othPics, othNoPics) = partition hasPicture others
+ mconcat <$> mapM shuffle [admPics, admNoPics, othPics, othNoPics]
+ where
+ isAdmin GroupMemberIntro {reMember = GroupMember {memberRole}} = memberRole >= GRAdmin
+ hasPicture GroupMemberIntro {reMember = GroupMember {memberProfile = LocalProfile {image}}} = isJust image
processIntro intro@GroupMemberIntro {introId} = do
void $ sendDirectMessage conn (XGrpMemIntro $ memberInfo (reMember intro)) (GroupId groupId)
withStore' $ \db -> updateIntroStatus db introId GMIntroSent
+ sendHistory =
+ when (isCompatibleRange (memberChatVRange' m) batchSendVRange) $ do
+ (errs, items) <- partitionEithers <$> withStore' (\db -> getGroupHistoryItems db user gInfo 100)
+ (errs', events) <- partitionEithers <$> mapM (tryChatError . itemForwardEvents) items
+ let errors = map ChatErrorStore errs <> errs'
+ unless (null errors) $ toView $ CRChatErrors (Just user) errors
+ forM_ (L.nonEmpty $ concat events) $ \events' ->
+ sendGroupMemberMessages user conn events' groupId
+ itemForwardEvents :: CChatItem 'CTGroup -> m [ChatMsgEvent 'Json]
+ itemForwardEvents cci = case cci of
+ (CChatItem SMDRcv ci@ChatItem {chatDir = CIGroupRcv sender, content = CIRcvMsgContent mc, file}) -> do
+ fInvDescr_ <- join <$> forM file getRcvFileInvDescr
+ processContentItem sender ci mc fInvDescr_
+ (CChatItem SMDSnd ci@ChatItem {content = CISndMsgContent mc, file}) -> do
+ fInvDescr_ <- join <$> forM file getSndFileInvDescr
+ processContentItem membership ci mc fInvDescr_
+ _ -> pure []
+ where
+ getRcvFileInvDescr :: CIFile 'MDRcv -> m (Maybe (FileInvitation, RcvFileDescrText))
+ getRcvFileInvDescr ciFile@CIFile {fileId, fileProtocol, fileStatus} = do
+ expired <- fileExpired
+ if fileProtocol /= FPXFTP || fileStatus == CIFSRcvCancelled || expired
+ then pure Nothing
+ else do
+ rfd <- withStore $ \db -> getRcvFileDescrByRcvFileId db fileId
+ pure $ invCompleteDescr ciFile rfd
+ getSndFileInvDescr :: CIFile 'MDSnd -> m (Maybe (FileInvitation, RcvFileDescrText))
+ getSndFileInvDescr ciFile@CIFile {fileId, fileProtocol, fileStatus} = do
+ expired <- fileExpired
+ if fileProtocol /= FPXFTP || fileStatus == CIFSSndCancelled || expired
+ then pure Nothing
+ else do
+ -- can also lookup in extra_xftp_file_descriptions, though it can be empty;
+ -- would be best if snd file had a single rcv description for all members saved in files table
+ rfd <- withStore $ \db -> getRcvFileDescrBySndFileId db fileId
+ pure $ invCompleteDescr ciFile rfd
+ fileExpired :: m Bool
+ fileExpired = do
+ ttl <- asks $ rcvFilesTTL . agentConfig . config
+ cutoffTs <- addUTCTime (-ttl) <$> liftIO getCurrentTime
+ pure $ chatItemTs cci < cutoffTs
+ invCompleteDescr :: CIFile d -> RcvFileDescr -> Maybe (FileInvitation, RcvFileDescrText)
+ invCompleteDescr CIFile {fileName, fileSize} RcvFileDescr {fileDescrText, fileDescrComplete}
+ | fileDescrComplete =
+ let fInvDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False}
+ fInv = xftpFileInvitation fileName fileSize fInvDescr
+ in Just (fInv, fileDescrText)
+ | otherwise = Nothing
+ processContentItem :: GroupMember -> ChatItem 'CTGroup d -> MsgContent -> Maybe (FileInvitation, RcvFileDescrText) -> m [ChatMsgEvent Json]
+ processContentItem sender ChatItem {meta, quotedItem} mc fInvDescr_ =
+ if isNothing fInvDescr_ && not (msgContentHasText mc)
+ then pure []
+ else do
+ let CIMeta {itemTs, itemSharedMsgId, itemTimed} = meta
+ quotedItemId_ = quoteItemId =<< quotedItem
+ fInv_ = fst <$> fInvDescr_
+ (msgContainer, _) <- prepareGroupMsg user gInfo mc quotedItemId_ fInv_ itemTimed False
+ let senderVRange = memberChatVRange' sender
+ xMsgNewChatMsg = ChatMessage {chatVRange = senderVRange, msgId = itemSharedMsgId, chatMsgEvent = XMsgNew msgContainer}
+ fileDescrEvents <- case (snd <$> fInvDescr_, itemSharedMsgId) of
+ (Just fileDescrText, Just msgId) -> do
+ parts <- splitFileDescr fileDescrText
+ pure . toList $ L.map (XMsgFileDescr msgId) parts
+ _ -> pure []
+ let fileDescrChatMsgs = map (ChatMessage senderVRange Nothing) fileDescrEvents
+ GroupMember {memberId} = sender
+ msgForwardEvents = map (\cm -> XGrpMsgForward memberId cm itemTs) (xMsgNewChatMsg : fileDescrChatMsgs)
+ pure msgForwardEvents
_ -> do
- -- TODO notify member who forwarded introduction - question - where it is stored? There is via_contact but probably there should be via_member in group_members table
let memCategory = memberCategory m
withStore' (\db -> getViaGroupContact db user m) >>= \case
Nothing -> do
@@ -3610,41 +3712,27 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
void $ sendDirectMessage imConn (XGrpMemCon m.memberId) (GroupId groupId)
_ -> messageWarning "sendXGrpMemCon: member category GCPreMember or GCPostMember is expected"
MSG msgMeta _msgFlags msgBody -> do
+ checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure ()
cmdId <- createAckCmd conn
- tryChatError (processChatMessage cmdId) >>= \case
- Right (ACMsg _ chatMsg, withRcpt) -> do
- ackMsg agentConnId cmdId msgMeta $ if withRcpt then Just "" else Nothing
- when (membership.memberRole >= GRAdmin) $ forwardMsg_ chatMsg
- Left e -> ackMsg agentConnId cmdId msgMeta Nothing >> throwError e
+ let aChatMsgs = parseChatMessages msgBody
+ withAckMessage agentConnId cmdId msgMeta $ do
+ forM_ aChatMsgs $ \case
+ Right (ACMsg _ chatMsg) ->
+ processEvent cmdId chatMsg `catchChatError` \e -> toView $ CRChatError (Just user) e
+ Left e -> toView $ CRChatError (Just user) (ChatError . CEException $ "error parsing chat message: " <> e)
+ checkSendRcpt $ rights aChatMsgs
+ -- currently only a single message is forwarded
+ when (membership.memberRole >= GRAdmin) $ case aChatMsgs of
+ [Right (ACMsg _ chatMsg)] -> forwardMsg_ chatMsg
+ _ -> pure ()
where
- processChatMessage :: Int64 -> m (AChatMessage, Bool)
- processChatMessage cmdId = do
- msg@(ACMsg _ chatMsg) <- parseAChatMessage conn msgMeta msgBody
- checkIntegrity chatMsg `catchChatError` \_ -> pure ()
- (msg,) <$> processEvent cmdId chatMsg
brokerTs = metaBrokerTs msgMeta
- checkIntegrity :: ChatMessage e -> m ()
- checkIntegrity ChatMessage {chatMsgEvent} = do
- when checkForEvent $ checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta
- where
- checkForEvent = case chatMsgEvent of
- XMsgNew _ -> True
- XFileCancel _ -> True
- XFileAcptInv {} -> True
- XGrpMemNew _ -> True
- XGrpMemRole {} -> True
- XGrpMemDel _ -> True
- XGrpLeave -> True
- XGrpDel -> True
- XGrpInfo _ -> True
- XGrpDirectInv {} -> True
- _ -> False
- processEvent :: MsgEncodingI e => CommandId -> ChatMessage e -> m Bool
+ processEvent :: MsgEncodingI e => CommandId -> ChatMessage e -> m ()
processEvent cmdId chatMsg = do
(m', conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveGroupRcvMsg user groupId m conn msgMeta cmdId msgBody chatMsg
updateChatLock "groupMessage" event
case event of
- XMsgNew mc -> memberCanSend m' $ newGroupContentMessage gInfo m' mc msg brokerTs
+ XMsgNew mc -> memberCanSend m' $ newGroupContentMessage gInfo m' mc msg brokerTs False
XMsgFileDescr sharedMsgId fileDescr -> memberCanSend m' $ groupMessageFileDescription gInfo m' sharedMsgId fileDescr
XMsgUpdate sharedMsgId mContent ttl live -> memberCanSend m' $ groupMessageUpdate gInfo m' sharedMsgId mContent msg brokerTs ttl live
XMsgDel sharedMsgId memberId -> groupMessageDelete gInfo m' sharedMsgId memberId msg brokerTs
@@ -3672,15 +3760,17 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
XInfoProbeOk probe -> xInfoProbeOk (COMGroupMember m') probe
BFileChunk sharedMsgId chunk -> bFileChunkGroup gInfo sharedMsgId chunk msgMeta
_ -> messageError $ "unsupported message: " <> T.pack (show event)
- checkSendRcpt event
- checkSendRcpt :: ChatMsgEvent e -> m Bool
- checkSendRcpt event = do
+ checkSendRcpt :: [AChatMessage] -> m Bool
+ checkSendRcpt aChatMsgs = do
currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo
let GroupInfo {chatSettings = ChatSettings {sendRcpts}} = gInfo
pure $
fromMaybe (sendRcptsSmallGroups user) sendRcpts
- && hasDeliveryReceipt (toCMEventTag event)
+ && any aChatMsgHasReceipt aChatMsgs
&& currentMemCount <= smallGroupsRcptsMemLimit
+ where
+ aChatMsgHasReceipt (ACMsg _ ChatMessage {chatMsgEvent}) =
+ hasDeliveryReceipt (toCMEventTag chatMsgEvent)
forwardMsg_ :: MsgEncodingI e => ChatMessage e -> m ()
forwardMsg_ chatMsg =
forM_ (forwardedGroupMsg chatMsg) $ \chatMsg' -> do
@@ -4017,15 +4107,11 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
ackMsgDeliveryEvent :: Connection -> CommandId -> m ()
ackMsgDeliveryEvent Connection {connId} ackCmdId =
- withStoreCtx'
- (Just $ "createRcvMsgDeliveryEvent, connId: " <> show connId <> ", ackCmdId: " <> show ackCmdId <> ", msgDeliveryStatus: MDSRcvAcknowledged")
- $ \db -> createRcvMsgDeliveryEvent db connId ackCmdId MDSRcvAcknowledged
+ withStore' $ \db -> updateRcvMsgDeliveryStatus db connId ackCmdId MDSRcvAcknowledged
sentMsgDeliveryEvent :: Connection -> AgentMsgId -> m ()
sentMsgDeliveryEvent Connection {connId} msgId =
- withStoreCtx
- (Just $ "createSndMsgDeliveryEvent, connId: " <> show connId <> ", msgId: " <> show msgId <> ", msgDeliveryStatus: MDSSndSent")
- $ \db -> createSndMsgDeliveryEvent db connId msgId MDSSndSent
+ withStore' $ \db -> updateSndMsgDeliveryStatus db connId msgId MDSSndSent
agentErrToItemStatus :: AgentErrorType -> CIStatus 'MDSnd
agentErrToItemStatus (SMP AUTH) = CISSndErrorAuth
@@ -4287,14 +4373,15 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
ChatErrorStore (SEChatItemSharedMsgIdNotFound sharedMsgId) -> handle sharedMsgId
e -> throwError e
- newGroupContentMessage :: GroupInfo -> GroupMember -> MsgContainer -> RcvMessage -> UTCTime -> m ()
- newGroupContentMessage gInfo m@GroupMember {memberId, memberRole} mc msg@RcvMessage {sharedMsgId_} brokerTs
+ newGroupContentMessage :: GroupInfo -> GroupMember -> MsgContainer -> RcvMessage -> UTCTime -> Bool -> m ()
+ newGroupContentMessage gInfo m@GroupMember {memberId, memberRole} mc msg@RcvMessage {sharedMsgId_} brokerTs forwarded
| isVoice content && not (groupFeatureAllowed SGFVoice gInfo) = rejected GFVoice
| not (isVoice content) && isJust fInv_ && not (groupFeatureAllowed SGFFiles gInfo) = rejected GFFiles
| otherwise = do
- -- TODO integrity message check
- -- check if message moderation event was received ahead of message
- let timed_ = rcvGroupCITimed gInfo itemTTL
+ let timed_ =
+ if forwarded
+ then rcvCITimed_ (Just Nothing) itemTTL
+ else rcvGroupCITimed gInfo itemTTL
live = fromMaybe False live_
withStore' (\db -> getCIModeration db user gInfo memberId sharedMsgId_) >>= \case
Just ciModeration -> do
@@ -5221,7 +5308,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
let body = LB.toStrict $ J.encode msg
rcvMsg@RcvMessage {chatMsgEvent = ACME _ event} <- saveGroupFwdRcvMsg user groupId m author body chatMsg
case event of
- XMsgNew mc -> memberCanSend author $ newGroupContentMessage gInfo author mc rcvMsg msgTs
+ XMsgNew mc -> memberCanSend author $ newGroupContentMessage gInfo author mc rcvMsg msgTs True
XMsgFileDescr sharedMsgId fileDescr -> memberCanSend author $ groupMessageFileDescription gInfo author sharedMsgId fileDescr
XMsgUpdate sharedMsgId mContent ttl live -> memberCanSend author $ groupMessageUpdate gInfo author sharedMsgId mContent rcvMsg msgTs ttl live
XMsgDel sharedMsgId memId -> groupMessageDelete gInfo author sharedMsgId memId rcvMsg msgTs
@@ -5240,14 +5327,19 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
directMsgReceived ct conn@Connection {connId} msgMeta msgRcpts = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
forM_ msgRcpts $ \MsgReceipt {agentMsgId, msgRcptStatus} -> do
- withStore $ \db -> createSndMsgDeliveryEvent db connId agentMsgId $ MDSSndRcvd msgRcptStatus
+ withStore' $ \db -> updateSndMsgDeliveryStatus db connId agentMsgId $ MDSSndRcvd msgRcptStatus
updateDirectItemStatus ct conn agentMsgId $ CISSndRcvd msgRcptStatus SSPComplete
+ -- TODO [batch send] update status of all messages in batch
+ -- - this is for when we implement identifying inactive connections
+ -- - regular messages sent in batch would all be marked as delivered by a single receipt
+ -- - repeat for directMsgReceived if same logic is applied to direct messages
+ -- - getChatItemIdByAgentMsgId to return [ChatItemId]
groupMsgReceived :: GroupInfo -> GroupMember -> Connection -> MsgMeta -> NonEmpty MsgReceipt -> m ()
groupMsgReceived gInfo m conn@Connection {connId} msgMeta msgRcpts = do
checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta
forM_ msgRcpts $ \MsgReceipt {agentMsgId, msgRcptStatus} -> do
- withStore $ \db -> createSndMsgDeliveryEvent db connId agentMsgId $ MDSSndRcvd msgRcptStatus
+ withStore' $ \db -> updateSndMsgDeliveryStatus db connId agentMsgId $ MDSSndRcvd msgRcptStatus
updateGroupItemStatus gInfo m conn agentMsgId $ CISSndRcvd msgRcptStatus SSPComplete
updateDirectItemStatus :: Contact -> Connection -> AgentMsgId -> CIStatus 'MDSnd -> m ()
@@ -5338,17 +5430,13 @@ sendFileInline_ FileTransferMeta {filePath, chunkSize} sharedMsgId sendMsg =
chSize = fromIntegral chunkSize
parseChatMessage :: ChatMonad m => Connection -> ByteString -> m (ChatMessage 'Json)
-parseChatMessage conn = parseChatMessage_ conn Nothing
-{-# INLINE parseChatMessage #-}
-
-parseAChatMessage :: ChatMonad m => Connection -> MsgMeta -> ByteString -> m AChatMessage
-parseAChatMessage conn msgMeta = parseChatMessage_ conn (Just msgMeta)
-{-# INLINE parseAChatMessage #-}
-
-parseChatMessage_ :: (ChatMonad m, StrEncoding s) => Connection -> Maybe MsgMeta -> ByteString -> m s
-parseChatMessage_ conn msgMeta s = liftEither . first (ChatError . errType) $ strDecode s
+parseChatMessage conn s = do
+ case parseChatMessages s of
+ [msg] -> liftEither . first (ChatError . errType) $ (\(ACMsg _ m) -> checkEncoding m) =<< msg
+ _ -> throwChatError $ CEException "parseChatMessage: single message is expected"
where
- errType = CEInvalidChatMessage conn (msgMetaToJson <$> msgMeta) (safeDecodeUtf8 s)
+ errType = CEInvalidChatMessage conn Nothing (safeDecodeUtf8 s)
+{-# INLINE parseChatMessage #-}
sendFileChunk :: ChatMonad m => User -> SndFileTransfer -> m ()
sendFileChunk user ft@SndFileTransfer {fileId, fileStatus, agentConnId = AgentConnId acId} =
@@ -5525,40 +5613,77 @@ createSndMessage :: (MsgEncodingI e, ChatMonad m) => ChatMsgEvent e -> ConnOrGro
createSndMessage chatMsgEvent connOrGroupId = do
gVar <- asks random
ChatConfig {chatVRange} <- asks config
- withStore $ \db -> createNewSndMessage db gVar connOrGroupId $ \sharedMsgId ->
- let msgBody = strEncode ChatMessage {chatVRange, msgId = Just sharedMsgId, chatMsgEvent}
- in NewMessage {chatMsgEvent, msgBody}
+ withStore $ \db -> createNewSndMessage db gVar connOrGroupId chatMsgEvent (encodeMessage chatVRange)
+ where
+ encodeMessage chatVRange sharedMsgId =
+ encodeChatMessage ChatMessage {chatVRange, msgId = Just sharedMsgId, chatMsgEvent}
+
+sendGroupMemberMessages :: forall e m. (MsgEncodingI e, ChatMonad m) => User -> Connection -> NonEmpty (ChatMsgEvent e) -> GroupId -> m ()
+sendGroupMemberMessages user conn@Connection {connId} events groupId = do
+ when (connDisabled conn) $ throwChatError (CEConnectionDisabled conn)
+ (errs, msgs) <- partitionEithers <$> createSndMessages
+ unless (null errs) $ toView $ CRChatErrors (Just user) errs
+ unless (null msgs) $ do
+ let (errs', msgBatches) = partitionEithers $ batchMessages maxChatMsgSize msgs
+ -- shouldn't happen, as large messages would have caused createNewSndMessage to throw SELargeMsg
+ unless (null errs') $ toView $ CRChatErrors (Just user) errs'
+ forM_ msgBatches $ \batch ->
+ processBatch batch `catchChatError` (toView . CRChatError (Just user))
+ where
+ processBatch :: MsgBatch -> m ()
+ processBatch (MsgBatch builder sndMsgs) = do
+ let batchBody = LB.toStrict $ toLazyByteString builder
+ agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) MsgFlags {notification = True} batchBody
+ let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId}
+ void . withStoreBatch' $ \db -> map (\SndMessage {msgId} -> createSndMsgDelivery db sndMsgDelivery msgId) sndMsgs
+ createSndMessages :: m [Either ChatError SndMessage]
+ createSndMessages = do
+ gVar <- asks random
+ ChatConfig {chatVRange} <- asks config
+ withStoreBatch $ \db -> map (createMsg db gVar chatVRange) (toList events)
+ createMsg db gVar chatVRange evnt = do
+ r <- runExceptT $ createNewSndMessage db gVar (GroupId groupId) evnt (encodeMessage chatVRange evnt)
+ pure $ first ChatErrorStore r
+ encodeMessage chatVRange evnt sharedMsgId =
+ encodeChatMessage ChatMessage {chatVRange, msgId = Just sharedMsgId, chatMsgEvent = evnt}
directMessage :: (MsgEncodingI e, ChatMonad m) => ChatMsgEvent e -> m ByteString
directMessage chatMsgEvent = do
ChatConfig {chatVRange} <- asks config
- pure $ strEncode ChatMessage {chatVRange, msgId = Nothing, chatMsgEvent}
+ let r = encodeChatMessage ChatMessage {chatVRange, msgId = Nothing, chatMsgEvent}
+ case r of
+ ECMEncoded encodedBody -> pure . LB.toStrict $ encodedBody
+ ECMLarge -> throwChatError $ CEException "large message"
-deliverMessage :: ChatMonad m => Connection -> CMEventTag e -> MsgBody -> MessageId -> m Int64
-deliverMessage conn cmEventTag msgBody msgId =
- deliverMessages [(conn, cmEventTag, msgBody, msgId)] >>= \case
+deliverMessage :: ChatMonad m => Connection -> CMEventTag e -> LazyMsgBody -> MessageId -> m Int64
+deliverMessage conn cmEventTag msgBody msgId = do
+ let msgFlags = MsgFlags {notification = hasNotification cmEventTag}
+ deliverMessage' conn msgFlags msgBody msgId
+
+deliverMessage' :: ChatMonad m => Connection -> MsgFlags -> LazyMsgBody -> MessageId -> m Int64
+deliverMessage' conn msgFlags msgBody msgId =
+ deliverMessages [(conn, msgFlags, msgBody, msgId)] >>= \case
[r] -> liftEither r
rs -> throwChatError $ CEInternalError $ "deliverMessage: expected 1 result, got " <> show (length rs)
-deliverMessages :: ChatMonad' m => [(Connection, CMEventTag e, MsgBody, MessageId)] -> m [Either ChatError Int64]
+deliverMessages :: ChatMonad' m => [(Connection, MsgFlags, LazyMsgBody, MessageId)] -> m [Either ChatError Int64]
deliverMessages msgReqs = do
sent <- zipWith prepareBatch msgReqs <$> withAgent' (`sendMessages` aReqs)
withStoreBatch $ \db -> map (bindRight $ createDelivery db) sent
where
- aReqs = map (\(conn, cmEvTag, msgBody, _msgId) -> (aConnId conn, msgFlags cmEvTag, msgBody)) msgReqs
- msgFlags cmEvTag = MsgFlags {notification = hasNotification cmEvTag}
+ aReqs = map (\(conn, msgFlags, msgBody, _msgId) -> (aConnId conn, msgFlags, LB.toStrict msgBody)) msgReqs
prepareBatch req = bimap (`ChatErrorAgent` Nothing) (req,)
- createDelivery :: DB.Connection -> ((Connection, CMEventTag e, MsgBody, MessageId), AgentMsgId) -> IO (Either ChatError Int64)
+ createDelivery :: DB.Connection -> ((Connection, MsgFlags, LazyMsgBody, MessageId), AgentMsgId) -> IO (Either ChatError Int64)
createDelivery db ((Connection {connId}, _, _, msgId), agentMsgId) =
Right <$> createSndMsgDelivery db (SndMsgDelivery {connId, agentMsgId}) msgId
sendGroupMessage :: (MsgEncodingI e, ChatMonad m) => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> m (SndMessage, [GroupMember])
sendGroupMessage user GroupInfo {groupId} members chatMsgEvent = do
msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent (GroupId groupId)
- recipientMembers <- liftIO $ shuffleMembers (filter memberCurrent members) $ \GroupMember {memberRole} -> memberRole
- let tag = toCMEventTag chatMsgEvent
+ recipientMembers <- liftIO $ shuffleMembers (filter memberCurrent members)
+ let msgFlags = MsgFlags {notification = hasNotification $ toCMEventTag chatMsgEvent}
(toSend, pending) = foldr addMember ([], []) recipientMembers
- msgReqs = map (\(_, conn) -> (conn, tag, msgBody, msgId)) toSend
+ msgReqs = map (\(_, conn) -> (conn, msgFlags, msgBody, msgId)) toSend
delivered <- deliverMessages msgReqs
let errors = lefts delivered
unless (null errors) $ toView $ CRChatErrors (Just user) errors
@@ -5566,6 +5691,12 @@ sendGroupMessage user GroupInfo {groupId} members chatMsgEvent = do
let sentToMembers = filterSent delivered toSend fst <> filterSent stored pending id
pure (msg, sentToMembers)
where
+ shuffleMembers :: [GroupMember] -> IO [GroupMember]
+ shuffleMembers ms = do
+ let (adminMs, otherMs) = partition isAdmin ms
+ liftM2 (<>) (shuffle adminMs) (shuffle otherMs)
+ where
+ isAdmin GroupMember {memberRole} = memberRole >= GRAdmin
addMember m (toSend, pending) = case memberSendAction chatMsgEvent members m of
Just (MSASend conn) -> ((m, conn) : toSend, pending)
Just MSAPending -> (toSend, m : pending)
@@ -5614,15 +5745,6 @@ sendGroupMemberMessage user m@GroupMember {groupMemberId} chatMsgEvent groupId i
MSASend conn -> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId >> postDeliver
MSAPending -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_
-shuffleMembers :: [a] -> (a -> GroupMemberRole) -> IO [a]
-shuffleMembers ms role = do
- let (adminMs, otherMs) = partition ((GRAdmin <=) . role) ms
- liftM2 (<>) (shuffle adminMs) (shuffle otherMs)
- where
- random :: IO Word16
- random = randomRIO (0, 65535)
- shuffle xs = map snd . sortBy (comparing fst) <$> mapM (\x -> (,x) <$> random) xs
-
sendPendingGroupMessages :: ChatMonad m => User -> GroupMember -> Connection -> m ()
sendPendingGroupMessages user GroupMember {groupMemberId, localDisplayName} conn = do
pendingMessages <- withStore' $ \db -> getPendingGroupMessages db groupMemberId
@@ -5639,21 +5761,25 @@ sendPendingGroupMessages user GroupMember {groupMemberId, localDisplayName} conn
_ -> throwChatError $ CEGroupMemberIntroNotFound localDisplayName
_ -> pure ()
+-- TODO [batch send] refactor direct message processing same as groups (e.g. checkIntegrity before processing)
saveDirectRcvMSG :: ChatMonad m => Connection -> MsgMeta -> CommandId -> MsgBody -> m (Connection, RcvMessage)
-saveDirectRcvMSG conn@Connection {connId} agentMsgMeta agentAckCmdId msgBody = do
- ACMsg _ ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent} <- parseAChatMessage conn agentMsgMeta msgBody
- conn' <- updatePeerChatVRange conn chatVRange
- let agentMsgId = fst $ recipient agentMsgMeta
- newMsg = NewMessage {chatMsgEvent, msgBody}
- rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta, agentAckCmdId}
- msg <- withStore $ \db -> createNewMessageAndRcvMsgDelivery db (ConnectionId connId) newMsg sharedMsgId_ rcvMsgDelivery Nothing
- pure (conn', msg)
+saveDirectRcvMSG conn@Connection {connId} agentMsgMeta agentAckCmdId msgBody =
+ case parseChatMessages msgBody of
+ [Right (ACMsg _ ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent})] -> do
+ conn' <- updatePeerChatVRange conn chatVRange
+ let agentMsgId = fst $ recipient agentMsgMeta
+ newMsg = NewRcvMessage {chatMsgEvent, msgBody}
+ rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta, agentAckCmdId}
+ msg <- withStore $ \db -> createNewMessageAndRcvMsgDelivery db (ConnectionId connId) newMsg sharedMsgId_ rcvMsgDelivery Nothing
+ pure (conn', msg)
+ [Left e] -> error $ "saveDirectRcvMSG: error parsing chat message: " <> e
+ _ -> error "saveDirectRcvMSG: batching not supported"
saveGroupRcvMsg :: (MsgEncodingI e, ChatMonad m) => User -> GroupId -> GroupMember -> Connection -> MsgMeta -> CommandId -> MsgBody -> ChatMessage e -> m (GroupMember, Connection, RcvMessage)
saveGroupRcvMsg user groupId authorMember conn@Connection {connId} agentMsgMeta agentAckCmdId msgBody ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent} = do
(am', conn') <- updateMemberChatVRange authorMember conn chatVRange
let agentMsgId = fst $ recipient agentMsgMeta
- newMsg = NewMessage {chatMsgEvent, msgBody}
+ newMsg = NewRcvMessage {chatMsgEvent, msgBody}
rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta, agentAckCmdId}
amId = Just am'.groupMemberId
msg <-
@@ -5669,7 +5795,7 @@ saveGroupRcvMsg user groupId authorMember conn@Connection {connId} agentMsgMeta
saveGroupFwdRcvMsg :: (MsgEncodingI e, ChatMonad m) => User -> GroupId -> GroupMember -> GroupMember -> MsgBody -> ChatMessage e -> m RcvMessage
saveGroupFwdRcvMsg user groupId forwardingMember refAuthorMember msgBody ChatMessage {msgId = sharedMsgId_, chatMsgEvent} = do
- let newMsg = NewMessage {chatMsgEvent, msgBody}
+ let newMsg = NewRcvMessage {chatMsgEvent, msgBody}
fwdMemberId = Just $ groupMemberId' forwardingMember
refAuthorId = Just $ groupMemberId' refAuthorMember
withStore (\db -> createNewRcvMessage db (GroupId groupId) newMsg sharedMsgId_ refAuthorId fwdMemberId)
@@ -6233,6 +6359,7 @@ chatCommandP =
"/set voice @" *> (SetContactFeature (ACF SCFVoice) <$> displayName <*> optional (A.space *> strP)),
"/set voice " *> (SetUserFeature (ACF SCFVoice) <$> strP),
"/set files #" *> (SetGroupFeature (AGF SGFFiles) <$> displayName <*> (A.space *> strP)),
+ "/set history #" *> (SetGroupFeature (AGF SGFHistory) <$> displayName <*> (A.space *> strP)),
"/set calls @" *> (SetContactFeature (ACF SCFCalls) <$> displayName <*> optional (A.space *> strP)),
"/set calls " *> (SetUserFeature (ACF SCFCalls) <$> strP),
"/set delete #" *> (SetGroupFeature (AGF SGFFullDelete) <$> displayName <*> (A.space *> strP)),
@@ -6320,7 +6447,12 @@ chatCommandP =
jsonP = J.eitherDecodeStrict' <$?> A.takeByteString
groupProfile = do
(gName, fullName) <- profileNames
- let groupPreferences = Just (emptyGroupPrefs :: GroupPreferences) {directMessages = Just DirectMessagesGroupPreference {enable = FEOn}}
+ let groupPreferences =
+ Just
+ (emptyGroupPrefs :: GroupPreferences)
+ { directMessages = Just DirectMessagesGroupPreference {enable = FEOn},
+ history = Just HistoryGroupPreference {enable = FEOn}
+ }
pure GroupProfile {displayName = gName, fullName, description = Nothing, image = Nothing, groupPreferences}
fullNameP = A.space *> textP <|> pure ""
textP = safeDecodeUtf8 <$> A.takeByteString
@@ -6358,6 +6490,7 @@ chatCommandP =
<|> ("day" $> 86400)
<|> ("week" $> (7 * 86400))
<|> ("month" $> (30 * 86400))
+ <|> A.decimal
timedTTLOnOffP =
optional ("on" *> A.space) *> (Just <$> timedTTLP)
<|> ("off" $> Nothing)
diff --git a/src/Simplex/Chat/Help.hs b/src/Simplex/Chat/Help.hs
index 5d0548ca3f..ac93e05533 100644
--- a/src/Simplex/Chat/Help.hs
+++ b/src/Simplex/Chat/Help.hs
@@ -155,7 +155,8 @@ groupsHelpInfo =
"",
green "Group chat preferences:",
indent <> highlight "/set voice # on/off " <> " - enable/disable voice messages",
- -- indent <> highlight "/set files # on/off " <> " - enable/disable files and media (other than voice)",
+ indent <> highlight "/set files # on/off " <> " - enable/disable files and media (other than voice)",
+ indent <> highlight "/set history # on/off " <> " - enable/disable sending recent history to new members",
indent <> highlight "/set delete # on/off " <> " - enable/disable full message deletion",
indent <> highlight "/set direct # on/off " <> " - enable/disable direct messages to other members",
indent <> highlight "/set disappear # on