mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Merge branch 'master' into f/batch-send
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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] = []
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -155,6 +155,34 @@ afterEvaluate {
|
||||
val endTagRegex = Regex("</")
|
||||
val anyHtmlRegex = Regex("[^>]*>.*(<|>).*</string>|[^>]*>.*(<|>).*</string>")
|
||||
val correctHtmlRegex = Regex("[^>]*>.*<b>.*</b>.*</string>|[^>]*>.*<i>.*</i>.*</string>|[^>]*>.*<u>.*</u>.*</string>|[^>]*>.*<font[^>]*>.*</font>.*</string>")
|
||||
val possibleFormat = listOf("s", "d", "1\$s", "1\$d", "2s", "f")
|
||||
|
||||
fun String.id(): String = replace("<string name=\"", "").trim().substringBefore("\"")
|
||||
|
||||
fun String.formatting(filepath: String): List<String> {
|
||||
if (!contains("%")) return emptyList()
|
||||
val value = substringAfter("\">").substringBeforeLast("</string>")
|
||||
|
||||
val formats = ArrayList<String>()
|
||||
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("<![CDATA")) {
|
||||
@@ -195,20 +223,44 @@ afterEvaluate {
|
||||
return this
|
||||
}
|
||||
val fileRegex = Regex("MR/../strings.xml$|MR/..-.../strings.xml$|MR/..-../strings.xml$|MR/base/strings.xml$")
|
||||
kotlin.sourceSets["commonMain"].resources.filter { fileRegex.containsMatchIn(it.absolutePath) }.asFileTree.forEach { file ->
|
||||
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<String, List<String>>()
|
||||
treeList.forEachIndexed { index, file ->
|
||||
val isBase = index == 0
|
||||
val initialLines = ArrayList<String>()
|
||||
val finalLines = ArrayList<String>()
|
||||
val errors = ArrayList<String>()
|
||||
|
||||
file.useLines { lines ->
|
||||
val multiline = ArrayList<String>()
|
||||
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 ->
|
||||
|
||||
+1
-1
@@ -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))
|
||||
}
|
||||
|
||||
@@ -150,7 +150,6 @@
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b> إضافة جهة اتصال جديدة </b>: لإنشاء رمز الاستجابة السريعة الخاص بك لمرة واحدة لجهة اتصالك.]]></string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b> امسح رمز الاستجابة السريعة </b>: للاتصال بجهة الاتصال التي تعرض لك رمز الاستجابة السريعة.]]></string>
|
||||
<string name="callstatus_in_progress">مكالمتك تحت الإجراء</string>
|
||||
<string name="callstatus_ended">انتهت المكالمة</string>
|
||||
<string name="change_database_passphrase_question">تغيير عبارة مرور قاعدة البيانات؟</string>
|
||||
<string name="cannot_access_keychain">لا يمكن الوصول إلى Keystore لحفظ كلمة مرور قاعدة البيانات</string>
|
||||
<string name="icon_descr_cancel_file_preview">إلغاء معاينة الملف</string>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<string name="accept">Αποδοχή</string>
|
||||
<string name="accept_connection_request__question">Αποδοχή αιτήματος σύνδεσης;</string>
|
||||
<string name="callstatus_accepted">αποδεκτή κλήση</string>
|
||||
<string name="network_enable_socks_info">Πρόσβαση στους διακομιστές μέσω SOCKS proxy στην πόρτα 9050; Ο διακομιστής μεσολάβησης (proxy server) πρέπει να είναι ενεργός πριν ενεργοποιηθεί αυτή η ρύθμιση.</string>
|
||||
<string name="network_enable_socks_info">Πρόσβαση στους διακομιστές μέσω SOCKS proxy στην πόρτα %d; Ο διακομιστής μεσολάβησης (proxy server) πρέπει να είναι ενεργός πριν ενεργοποιηθεί αυτή η ρύθμιση.</string>
|
||||
<string name="smp_servers_add">Προσθήκη διακομιστή…</string>
|
||||
<string name="network_settings">Προχωρημένες ρυθμίσεις δικτύου</string>
|
||||
<string name="v4_3_improved_server_configuration_desc">Προσθήκη διακομιστών μέσω σάρωσης QR κωδικών.</string>
|
||||
|
||||
@@ -298,7 +298,7 @@
|
||||
<string name="icon_descr_cancel_live_message">Cancelar mensaje en directo</string>
|
||||
<string name="confirm_verb">Confirmar</string>
|
||||
<string name="clear_chat_menu_action">Vaciar</string>
|
||||
<string name="app_version_code">Build de la aplicación</string>
|
||||
<string name="app_version_code">Build de la aplicación: %s</string>
|
||||
<string name="call_already_ended">¡La llamada ha terminado!</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">el servidor de envío ha cambiado para tí</string>
|
||||
<string name="icon_descr_cancel_link_preview">cancelar vista previa del enlace</string>
|
||||
|
||||
@@ -171,7 +171,6 @@
|
||||
<string name="chat_archive_header">Arkisto</string>
|
||||
<string name="delete_chat_archive_question">Poista keskusteluarkisto\?</string>
|
||||
<string name="archive_created_on_ts">Luotu %1$s</string>
|
||||
<string name="rcv_group_event_changed_your_role">%s:n rooli muutettu %s:ksi</string>
|
||||
<string name="rcv_group_event_group_deleted">poistettu ryhmä</string>
|
||||
<string name="group_member_status_connecting">yhdistää</string>
|
||||
<string name="group_member_status_accepted">yhdistäminen (hyväksytty)</string>
|
||||
|
||||
@@ -687,7 +687,6 @@
|
||||
<string name="sender_cancelled_file_transfer">ファイル送信が中止されました。</string>
|
||||
<string name="sender_may_have_deleted_the_connection_request">送信元が繋がりリクエストを削除したかもしれません。</string>
|
||||
<string name="error_smp_test_server_auth">このサーバで待ち行列を作るには認証が必要です。パスワードをご確認ください。</string>
|
||||
<string name="periodic_notifications_desc">アプリが定期的に新しいメッセージを受信します。一日の電池使用量が約3%で、プッシュ通知に頼らずに、あなたの端末のデータをサーバに送ることはありません。</string>
|
||||
<string name="la_notice_title_simplex_lock">SimpleXロック</string>
|
||||
<string name="enter_passphrase_notification_desc">通知を受けるには、データベースの暗証フレーズを入力してください。</string>
|
||||
<string name="simplex_service_notification_title">SimpleX Chat サービス</string>
|
||||
@@ -904,7 +903,6 @@
|
||||
<string name="settings_section_title_support">SIMPLEX CHATを支援</string>
|
||||
<string name="smp_servers_test_servers">テストサーバ</string>
|
||||
<string name="switch_receiving_address_desc">受信アドレスは別のサーバーに変更されます。アドレス変更は送信者がオンラインになった後に完了します。</string>
|
||||
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[あなたのプライバシーを守るために、このアプリはプッシュ通知の変わりに <b>SimpleX バックグラウンド・サービス</b> を使ってます。一日の電池使用量は約3%です。]]></string>
|
||||
<string name="to_protect_privacy_simplex_has_ids_for_queues">あなたのプライバシーを守るために、他のアプリと違って、ユーザーIDの変わりに SimpleX メッセージ束毎にIDを配布し、各連絡先が別々と扱います。</string>
|
||||
<string name="group_main_profile_sent">あなたのチャットプロフィールが他のグループメンバーに送られます。</string>
|
||||
<string name="to_verify_compare">エンドツーエンド暗号化を確認するには、ご自分の端末と連絡先の端末のコードを比べます (スキャンします)。</string>
|
||||
|
||||
@@ -337,7 +337,7 @@
|
||||
<string name="error_sending_message">Mesaj gönderilirken hata oluştu</string>
|
||||
<string name="error_creating_address">Adres oluştururken hata oluştu</string>
|
||||
<string name="error_changing_address">Adres değiştirirken hata oluştu</string>
|
||||
<string name="contact_wants_to_connect_via_call">1$s sizinle şu yolla bağlantı kurmak istiyor</string>
|
||||
<string name="contact_wants_to_connect_via_call">%1$s sizinle şu yolla bağlantı kurmak istiyor</string>
|
||||
<string name="error_changing_message_deletion">Ayarları değiştirirken hata oluştu</string>
|
||||
<string name="error_creating_link_for_group">Toplu konuşma bağlantısı oluştururken hata oluştu</string>
|
||||
<string name="error_changing_role">Yetki değiştirirken hata oluştu</string>
|
||||
@@ -747,9 +747,9 @@
|
||||
<string name="impossible_to_recover_passphrase"><![CDATA[<b>Aklınızda bulunsun</b>: kaybederseniz, parolayı kurtaramaz veya değiştiremezsiniz.]]></string>
|
||||
<string name="chat_archive_header">Sohbet arşivi</string>
|
||||
<string name="chat_archive_section">SOHBET ARŞİVİ</string>
|
||||
<string name="group_invitation_item_description">1$s grubuna davet</string>
|
||||
<string name="group_invitation_item_description">%1$s grubuna davet</string>
|
||||
<string name="join_group_question">Gruba katıl\?</string>
|
||||
<string name="rcv_group_event_member_added">1$s davet edildi</string>
|
||||
<string name="rcv_group_event_member_added">%1$s davet edildi</string>
|
||||
<string name="rcv_group_event_invited_via_your_group_link">grup bağlantınız üzerinden davet edildi</string>
|
||||
<string name="group_member_status_invited">davet edildi</string>
|
||||
<string name="invite_to_group_button">Gruba davet edin</string>
|
||||
|
||||
@@ -1344,7 +1344,7 @@
|
||||
<string name="v5_2_message_delivery_receipts_descr">我们错过的第二个\"√\"!✅</string>
|
||||
<string name="setup_database_passphrase">设定数据库密码</string>
|
||||
<string name="receipts_groups_title_disable">为群组禁用回执吗?</string>
|
||||
<string name="rcv_group_event_3_members_connected">%s、%s 和 %d 已连接</string>
|
||||
<string name="rcv_group_event_3_members_connected">%s、%s 和 %s 已连接</string>
|
||||
<string name="fix_connection_not_supported_by_group_member">修复群组成员不支持的问题</string>
|
||||
<string name="receipts_groups_override_enabled">已为 %d 组启用送达回执功能</string>
|
||||
<string name="sync_connection_force_confirm">重新协商</string>
|
||||
@@ -1427,7 +1427,6 @@
|
||||
<string name="connect_plan_connect_via_link">通过链接进行连接吗?</string>
|
||||
<string name="connect_plan_already_joining_the_group">已经加入了该群组!</string>
|
||||
<string name="group_members_n">%s、 %s 和 %d 名成员</string>
|
||||
<string name="moderated_items_description">%s 审核了 %d 条消息</string>
|
||||
<string name="unblock_member_button">解封成员</string>
|
||||
<string name="connect_plan_connect_to_yourself">连接到你自己?</string>
|
||||
<string name="contact_tap_to_connect">轻按连接</string>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<string name="about_simplex_chat">關於 SimpleX Chat</string>
|
||||
<string name="accept_connection_request__question">接受連接請求?</string>
|
||||
<string name="callstatus_accepted">已接受通話</string>
|
||||
<string name="network_enable_socks_info">要在端口啟用 SOCKS 代理伺服器嗎?在啟用這個選項之前,必須先啟用代理伺服器。</string>
|
||||
<string name="network_enable_socks_info">要在端口啟用 SOCKS 代理伺服器嗎 %d?在啟用這個選項之前,必須先啟用代理伺服器。</string>
|
||||
<string name="group_member_role_admin">管理員</string>
|
||||
<string name="above_then_preposition_continuation">然後,選按:</string>
|
||||
<string name="smp_servers_preset_add">新增預設伺服器</string>
|
||||
|
||||
+5
-6
@@ -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}")
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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.*
|
||||
|
||||
@@ -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";
|
||||
|
||||
+7
-7
@@ -200,7 +200,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.*
|
||||
@@ -260,7 +260,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.*
|
||||
@@ -320,7 +320,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.*
|
||||
@@ -382,7 +382,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.*
|
||||
@@ -443,7 +443,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.*
|
||||
@@ -509,7 +509,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.*
|
||||
@@ -603,7 +603,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.*
|
||||
|
||||
+75
-44
@@ -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)
|
||||
@@ -477,12 +477,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
|
||||
@@ -2262,11 +2264,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
|
||||
@@ -2289,16 +2294,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
|
||||
@@ -5086,7 +5091,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"
|
||||
@@ -5680,23 +5685,34 @@ deliverMessage conn cmEventTag msgBody msgId = do
|
||||
deliverMessage' conn msgFlags msgBody msgId
|
||||
|
||||
deliverMessage' :: ChatMonad m => Connection -> MsgFlags -> MsgBody -> MessageId -> m Int64
|
||||
deliverMessage' conn@Connection {connId} msgFlags msgBody msgId = do
|
||||
agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) msgFlags msgBody
|
||||
let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId}
|
||||
withStore' $ \db -> createSndMsgDelivery db sndMsgDelivery msgId
|
||||
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, MsgFlags, 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, msgFlags, msgBody, _msgId) -> (aConnId conn, msgFlags, msgBody)) msgReqs
|
||||
prepareBatch req = bimap (`ChatErrorAgent` Nothing) (req,)
|
||||
createDelivery :: DB.Connection -> ((Connection, MsgFlags, 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)
|
||||
rs <- forM recipientMembers $ \m ->
|
||||
messageMember m msg `catchChatError` (\e -> toView (CRChatError (Just user) e) $> Nothing)
|
||||
let sentToMembers = catMaybes rs
|
||||
let msgFlags = MsgFlags {notification = hasNotification $ toCMEventTag chatMsgEvent}
|
||||
(toSend, pending) = foldr addMember ([], []) recipientMembers
|
||||
msgReqs = map (\(_, conn) -> (conn, msgFlags, 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
|
||||
shuffleMembers :: [GroupMember] -> IO [GroupMember]
|
||||
@@ -5705,26 +5721,31 @@ sendGroupMessage' user members chatMsgEvent groupId introId_ postDeliver = do
|
||||
liftM2 (<>) (shuffle adminMs) (shuffle otherMs)
|
||||
where
|
||||
isAdmin GroupMember {memberRole} = memberRole >= GRAdmin
|
||||
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
|
||||
@@ -5738,6 +5759,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_
|
||||
|
||||
sendPendingGroupMessages :: ChatMonad m => User -> GroupMember -> Connection -> m ()
|
||||
sendPendingGroupMessages user GroupMember {groupMemberId, localDisplayName} conn = do
|
||||
pendingMessages <- withStore' $ \db -> getPendingGroupMessages db groupMemberId
|
||||
|
||||
@@ -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
|
||||
@@ -1288,12 +1289,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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
+3
-2
@@ -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
|
||||
@@ -353,6 +353,7 @@ serverCfg =
|
||||
serverStatsBackupFile = Nothing,
|
||||
smpServerVRange = supportedSMPServerVRange,
|
||||
transportConfig = defaultTransportServerConfig,
|
||||
smpHandshakeTimeout = 1000000,
|
||||
controlPort = Nothing
|
||||
}
|
||||
|
||||
|
||||
+41
-26
@@ -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:")
|
||||
|
||||
Reference in New Issue
Block a user