Compare commits

..

34 Commits

Author SHA1 Message Date
Avently 677eedfa8d android: don't show alert when not needed after changing user concurrently 2024-01-10 22:06:23 +07:00
Evgeny Poberezkin 25a4719414 core: 5.5.0.1 2024-01-10 12:06:29 +00:00
Evgeny Poberezkin f802ec75fe Merge branch 'stable' 2024-01-10 12:05:00 +00:00
Evgeny Poberezkin 045b195483 5.4.3: ios 188, android 169, desktop 22 2024-01-10 11:56:57 +00:00
spaced4ndy 283c90f5ae core: fix db method reserving extra local display name (#3659)
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2024-01-10 11:09:09 +04:00
Stanislav Dmitrenko 99a9fb2e1f ios: self destruct improvements (#3640)
* ios: self destruct improvements

* test

* adapted to stopped chat

* wait until ctrl initialization finishes

* Revert "test"

This reverts commit 7c199293cc.

* refactor

* simplify,fix

* refactor2

* refactor3

* comment

* fix

* fix

* comment

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>

* flip and rename flag

---------

Co-authored-by: Avently <avently@local>
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-01-09 21:01:41 +00:00
Evgeny Poberezkin ce9d583b39 Merge branch 'stable' 2024-01-09 20:36:30 +00:00
Evgeny Poberezkin 53414608db core: 5.4.3.0 (simplexmq 5.5.0.5) 2024-01-09 20:20:14 +00:00
Stanislav Dmitrenko c7cf206585 android: fix call sound when the app in the background (#3660)
* android: fix call sound when the app in the background

* using previous notification channel

* Revert "using previous notification channel"

This reverts commit 19da9a9ce193c39b353f478e884a97bbbf002e77.

* prevent playing sound on call twice
2024-01-09 19:45:46 +00:00
Evgeny Poberezkin 6067ac3c93 ios: more aggressive GC in NSE 2024-01-09 19:34:54 +00:00
Evgeny Poberezkin fc56873f1c ios: update core library 2024-01-09 19:33:55 +00:00
Stanislav Dmitrenko a30da38af7 android, desktop: accept calls after restart (#3662) 2024-01-09 19:31:01 +00:00
Stanislav Dmitrenko 0bf3d054c6 mobile, desktop: invalid display name alert (#3664)
* ios: invalid display name alert

* android, desktop: invalid display name

---------

Co-authored-by: Avently <avently@local>
2024-01-09 19:26:47 +00:00
Stanislav Dmitrenko a55a8b116a script: changes in script for downloading libs (#3663)
* script: changes in script for downloading libs

* ios script
2024-01-09 17:23:20 +00:00
Stanislav Dmitrenko 7a207fd641 android, desktop: alerts when device was disconnected (#3483) 2024-01-09 14:21:29 +00:00
Evgeny Poberezkin 4508e0dfc1 Merge branch 'stable' 2024-01-09 11:07:04 +00:00
Stanislav Dmitrenko a853ba3a15 android, desktop: adapted code for self destruct for ios logic (#3643)
* android, desktop: adapted code for self destruct for ios logic

* init db in case of periodic && self destruct enabled
2024-01-09 09:59:21 +00:00
Evgeny Poberezkin a2f190a6c6 core: update simplexmq (better batching) 2024-01-09 09:15:35 +00:00
Stanislav Dmitrenko 267178dddb android, desktop: show alerts on critical and internal errors (#3653)
* android, desktop: show alerts on critical and internal errors

* test

* don't stop chat if it's stopped already

* show notification

* restart chat or app

* Revert "test"

This reverts commit 5b78bbae5b.

* update strings

* strings2

* refactoring

* refactoring2

* refactoring3

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2024-01-08 18:20:52 +00:00
spaced4ndy fadce0c140 core: create new chat controller with chatActivated set to true 2024-01-08 17:34:10 +04:00
spaced4ndy 58ad97fe6d core: pause cleanup when chat is suspended (#3658) 2024-01-08 17:28:01 +04:00
Evgeny Poberezkin 3ccd9903a7 core: do not start clean up manager in background NSE (#3657)
* core: do not start clean up manager in background NSE

* update UIs

* fix test
2024-01-08 12:53:16 +00:00
Evgeny Poberezkin e294999044 ios: fix callkit calls via NSE (#3655)
* ios: fix callkit calls via NSE

* comments

* more reliable NSE start

* remove public logs, different RTS parameters for NSE

* only suspend NSE if we have chat controller, to avoid crashes if suspension attempted without controller created

* comments

* fix

* simplify
2024-01-08 10:56:01 +00:00
Evgeny Poberezkin 2bbc687f4a core: simplexmq 5.5.0.4 2024-01-06 11:48:28 +00:00
Evgeny Poberezkin 61c507e7da core: replace deprecated memcpy (#3652) 2024-01-06 11:32:26 +00:00
Evgeny Poberezkin 64230f3545 Merge branch 'stable' 2024-01-05 20:07:35 +00:00
Evgeny Poberezkin bb61b9c658 core: update simplexmq (critical errors, worker restarts, subscription timeouts) 2024-01-05 20:07:19 +00:00
Evgeny Poberezkin 3428f4d2ee core: update simplexmq (critical errors, worker restarts, subscription timeouts) 2024-01-05 18:51:18 +00:00
Stanislav Dmitrenko fe865c5e11 android, desktop: consistent colors in themes (#3649) 2024-01-05 18:45:52 +00:00
spaced4ndy 9e87fe73a5 core: batch send profile update (#3618)
* core: batch send profile update

* redundant

* reorder

* remove type

* createSndMessages

* refactor

* batched create internal item

* create feature items for multiple contacts

* comments

* refactor call site

* synonim

* refactor createSndMessages

* more batching

* remove partitionWith

* unite filter and fold

* refactor

* refactor

* refactor

* fix merge

* add test

* rename

* refactor

* refactor

* withExceptT

* refactor

* refactor2

* remove notChanged

* deliver with sendMessagesB (#3646)

* deliver with sendMessagesB

* refactor

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com>
2024-01-05 11:35:48 +04:00
Evgeny Poberezkin 0ef2c55983 core: invalid name error when it matches hidden profile (#3647) 2024-01-04 19:29:28 +00:00
spaced4ndy 8882284fb7 core: always check integrity on MSG in direct chats (#3641) 2024-01-04 16:22:16 +04:00
sh 575d899f5a build-android: fix new arrangement of nix command (#3634) 2024-01-02 14:39:23 +00:00
sh 825257e898 build-android: update nix and add armv7a branch switch (#3612)
* build-android: build armv7a in seperate tag

And update nix install.

* build-android: change tag detection logic

* build-android: minor change of logic
2023-12-30 10:19:56 +00:00
97 changed files with 1075 additions and 441 deletions
+6 -2
View File
@@ -31,6 +31,7 @@ struct ContentView: View {
@State private var showWhatsNew = false @State private var showWhatsNew = false
@State private var showChooseLAMode = false @State private var showChooseLAMode = false
@State private var showSetPasscode = false @State private var showSetPasscode = false
@State private var waitingForOrPassedAuth = true
@State private var chatListActionSheet: ChatListActionSheet? = nil @State private var chatListActionSheet: ChatListActionSheet? = nil
private enum ChatListActionSheet: Identifiable { private enum ChatListActionSheet: Identifiable {
@@ -61,6 +62,10 @@ struct ContentView: View {
} }
if !showSettings, let la = chatModel.laRequest { if !showSettings, let la = chatModel.laRequest {
LocalAuthView(authRequest: la) LocalAuthView(authRequest: la)
.onDisappear {
// this flag is separate from accessAuthenticated to show initializationView while we wait for authentication
waitingForOrPassedAuth = accessAuthenticated
}
} else if showSetPasscode { } else if showSetPasscode {
SetAppPasscodeView { SetAppPasscodeView {
chatModel.contentViewAccessAuthenticated = true chatModel.contentViewAccessAuthenticated = true
@@ -73,8 +78,7 @@ struct ContentView: View {
showSetPasscode = false showSetPasscode = false
alertManager.showAlert(laPasscodeNotSetAlert()) alertManager.showAlert(laPasscodeNotSetAlert())
} }
} } else if chatModel.chatDbStatus == nil && AppChatState.shared.value != .stopped && waitingForOrPassedAuth {
if chatModel.chatDbStatus == nil {
initializationView() initializationView()
} }
} }
+1
View File
@@ -54,6 +54,7 @@ final class ChatModel: ObservableObject {
@Published var chatDbChanged = false @Published var chatDbChanged = false
@Published var chatDbEncrypted: Bool? @Published var chatDbEncrypted: Bool?
@Published var chatDbStatus: DBMigrationResult? @Published var chatDbStatus: DBMigrationResult?
@Published var ctrlInitInProgress: Bool = false
// local authentication // local authentication
@Published var contentViewAccessAuthenticated: Bool = false @Published var contentViewAccessAuthenticated: Bool = false
@Published var laRequest: LocalAuthRequest? @Published var laRequest: LocalAuthRequest?
+3 -3
View File
@@ -16,13 +16,13 @@ private var nseSubscribers: [UUID:NSESubscriber] = [:]
private let SUSPENDING_TIMEOUT: TimeInterval = 2 private let SUSPENDING_TIMEOUT: TimeInterval = 2
// timeout should be larger than SUSPENDING_TIMEOUT // timeout should be larger than SUSPENDING_TIMEOUT
func waitNSESuspended(timeout: TimeInterval, dispatchQueue: DispatchQueue = DispatchQueue.main, suspended: @escaping (Bool) -> Void) { func waitNSESuspended(timeout: TimeInterval, suspended: @escaping (Bool) -> Void) {
if timeout <= SUSPENDING_TIMEOUT { if timeout <= SUSPENDING_TIMEOUT {
logger.warning("waitNSESuspended: small timeout \(timeout), using \(SUSPENDING_TIMEOUT + 1)") logger.warning("waitNSESuspended: small timeout \(timeout), using \(SUSPENDING_TIMEOUT + 1)")
} }
var state = nseStateGroupDefault.get() var state = nseStateGroupDefault.get()
if case .suspended = state { if case .suspended = state {
dispatchQueue.async { suspended(true) } DispatchQueue.main.async { suspended(true) }
return return
} }
let id = UUID() let id = UUID()
@@ -45,7 +45,7 @@ func waitNSESuspended(timeout: TimeInterval, dispatchQueue: DispatchQueue = Disp
logger.debug("waitNSESuspended notifySuspended: calling suspended(\(ok))") logger.debug("waitNSESuspended notifySuspended: calling suspended(\(ok))")
suspendedCalled = true suspendedCalled = true
nseSubscribers.removeValue(forKey: id) nseSubscribers.removeValue(forKey: id)
dispatchQueue.async { suspended(ok) } DispatchQueue.main.async { suspended(ok) }
} }
} }
+4 -2
View File
@@ -211,7 +211,7 @@ func apiDeleteUser(_ userId: Int64, _ delSMPQueues: Bool, viewPwd: String?) asyn
} }
func apiStartChat() throws -> Bool { func apiStartChat() throws -> Bool {
let r = chatSendCmdSync(.startChat(subscribe: true, expire: true, xftp: true)) let r = chatSendCmdSync(.startChat(mainApp: true))
switch r { switch r {
case .chatStarted: return true case .chatStarted: return true
case .chatRunning: return false case .chatRunning: return false
@@ -403,7 +403,7 @@ func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode) {
case let .ntfToken(token, status, ntfMode): return (token, status, ntfMode) case let .ntfToken(token, status, ntfMode): return (token, status, ntfMode)
case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off) case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off)
default: default:
logger.debug("apiGetNtfToken response: \(String(describing: r), privacy: .public)") logger.debug("apiGetNtfToken response: \(String(describing: r))")
return (nil, nil, .off) return (nil, nil, .off)
} }
} }
@@ -1215,6 +1215,8 @@ private func currentUserId(_ funcName: String) throws -> Int64 {
func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = nil, refreshInvitations: Bool = true, confirmMigrations: MigrationConfirmation? = nil) throws { func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = nil, refreshInvitations: Bool = true, confirmMigrations: MigrationConfirmation? = nil) throws {
logger.debug("initializeChat") logger.debug("initializeChat")
let m = ChatModel.shared let m = ChatModel.shared
m.ctrlInitInProgress = true
defer { m.ctrlInitInProgress = false }
(m.chatDbEncrypted, m.chatDbStatus) = chatMigrateInit(dbKey, confirmMigrations: confirmMigrations) (m.chatDbEncrypted, m.chatDbStatus) = chatMigrateInit(dbKey, confirmMigrations: confirmMigrations)
if m.chatDbStatus != .ok { return } if m.chatDbStatus != .ok { return }
// If we migrated successfully means previous re-encryption process on database level finished successfully too // If we migrated successfully means previous re-encryption process on database level finished successfully too
+19 -4
View File
@@ -19,11 +19,13 @@ let terminationTimeout: Int = 3 // seconds
let activationDelay: TimeInterval = 1.5 let activationDelay: TimeInterval = 1.5
let nseSuspendTimeout: TimeInterval = 5
private func _suspendChat(timeout: Int) { private func _suspendChat(timeout: Int) {
// this is a redundant check to prevent logical errors, like the one fixed in this PR // this is a redundant check to prevent logical errors, like the one fixed in this PR
let state = AppChatState.shared.value let state = AppChatState.shared.value
if !state.canSuspend { if !state.canSuspend {
logger.error("_suspendChat called, current state: \(state.rawValue, privacy: .public)") logger.error("_suspendChat called, current state: \(state.rawValue)")
} else if ChatModel.ok { } else if ChatModel.ok {
AppChatState.shared.set(.suspending) AppChatState.shared.set(.suspending)
apiSuspendChat(timeoutMicroseconds: timeout * 1000000) apiSuspendChat(timeoutMicroseconds: timeout * 1000000)
@@ -124,20 +126,33 @@ func initChatAndMigrate(refreshInvitations: Bool = true) {
} }
} }
func startChatAndActivate(dispatchQueue: DispatchQueue = DispatchQueue.main, _ completion: @escaping () -> Void) { func startChatForCall() {
logger.debug("DEBUGGING: startChatForCall")
if ChatModel.shared.chatRunning == true {
ChatReceiver.shared.start()
logger.debug("DEBUGGING: startChatForCall: after ChatReceiver.shared.start")
}
if .active != AppChatState.shared.value {
logger.debug("DEBUGGING: startChatForCall: before activateChat")
activateChat()
logger.debug("DEBUGGING: startChatForCall: after activateChat")
}
}
func startChatAndActivate(_ completion: @escaping () -> Void) {
logger.debug("DEBUGGING: startChatAndActivate") logger.debug("DEBUGGING: startChatAndActivate")
if ChatModel.shared.chatRunning == true { if ChatModel.shared.chatRunning == true {
ChatReceiver.shared.start() ChatReceiver.shared.start()
logger.debug("DEBUGGING: startChatAndActivate: after ChatReceiver.shared.start") logger.debug("DEBUGGING: startChatAndActivate: after ChatReceiver.shared.start")
} }
if .active == AppChatState.shared.value { if case .active = AppChatState.shared.value {
completion() completion()
} else if nseStateGroupDefault.get().inactive { } else if nseStateGroupDefault.get().inactive {
activate() activate()
} else { } else {
// setting app state to "activating" to notify NSE that it should suspend // setting app state to "activating" to notify NSE that it should suspend
setAppState(.activating) setAppState(.activating)
waitNSESuspended(timeout: 10, dispatchQueue: dispatchQueue) { ok in waitNSESuspended(timeout: nseSuspendTimeout) { ok in
if !ok { if !ok {
// if for some reason NSE failed to suspend, // if for some reason NSE failed to suspend,
// e.g., it crashed previously without setting its state to "suspended", // e.g., it crashed previously without setting its state to "suspended",
+7 -5
View File
@@ -44,8 +44,10 @@ struct SimpleXApp: App {
chatModel.appOpenUrl = url chatModel.appOpenUrl = url
} }
.onAppear() { .onAppear() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { if kcAppPassword.get() == nil || kcSelfDestructPassword.get() == nil {
initChatAndMigrate() DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
initChatAndMigrate()
}
} }
} }
.onChange(of: scenePhase) { phase in .onChange(of: scenePhase) { phase in
@@ -98,12 +100,12 @@ struct SimpleXApp: App {
if legacyDatabase, case .documents = dbContainerGroupDefault.get() { if legacyDatabase, case .documents = dbContainerGroupDefault.get() {
dbContainerGroupDefault.set(.documents) dbContainerGroupDefault.set(.documents)
setMigrationState(.offer) setMigrationState(.offer)
logger.debug("SimpleXApp init: using legacy DB in documents folder: \(getAppDatabasePath(), privacy: .public)*.db") logger.debug("SimpleXApp init: using legacy DB in documents folder: \(getAppDatabasePath())*.db")
} else { } else {
dbContainerGroupDefault.set(.group) dbContainerGroupDefault.set(.group)
setMigrationState(.ready) setMigrationState(.ready)
logger.debug("SimpleXApp init: using DB in app group container: \(getAppDatabasePath(), privacy: .public)*.db") logger.debug("SimpleXApp init: using DB in app group container: \(getAppDatabasePath())*.db")
logger.debug("SimpleXApp init: legacy DB\(legacyDatabase ? "" : " not", privacy: .public) present") logger.debug("SimpleXApp init: legacy DB\(legacyDatabase ? "" : " not") present")
} }
} }
@@ -38,13 +38,13 @@ struct ActiveCallView: View {
} }
} }
.onAppear { .onAppear {
logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase), privacy: .public), canConnectCall \(canConnectCall)") logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase)), canConnectCall \(canConnectCall)")
AppDelegate.keepScreenOn(true) AppDelegate.keepScreenOn(true)
createWebRTCClient() createWebRTCClient()
dismissAllSheets() dismissAllSheets()
} }
.onChange(of: canConnectCall) { _ in .onChange(of: canConnectCall) { _ in
logger.debug("ActiveCallView: canConnectCall changed to \(canConnectCall, privacy: .public)") logger.debug("ActiveCallView: canConnectCall changed to \(canConnectCall)")
createWebRTCClient() createWebRTCClient()
} }
.onDisappear { .onDisappear {
+41 -29
View File
@@ -130,7 +130,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
// The delay allows to accept the second call before suspending a chat // The delay allows to accept the second call before suspending a chat
// see `.onChange(of: scenePhase)` in SimpleXApp // see `.onChange(of: scenePhase)` in SimpleXApp
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
logger.debug("CallController: shouldSuspendChat \(String(describing: self?.shouldSuspendChat), privacy: .public)") logger.debug("CallController: shouldSuspendChat \(String(describing: self?.shouldSuspendChat))")
if ChatModel.shared.activeCall == nil && self?.shouldSuspendChat == true { if ChatModel.shared.activeCall == nil && self?.shouldSuspendChat == true {
self?.shouldSuspendChat = false self?.shouldSuspendChat = false
suspendChat() suspendChat()
@@ -142,45 +142,57 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
@objc(pushRegistry:didUpdatePushCredentials:forType:) @objc(pushRegistry:didUpdatePushCredentials:forType:)
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) { func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
logger.debug("CallController: didUpdate push credentials for type \(type.rawValue, privacy: .public)") logger.debug("CallController: didUpdate push credentials for type \(type.rawValue)")
} }
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) { func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
logger.debug("CallController: did receive push with type \(type.rawValue, privacy: .public)") logger.debug("CallController: did receive push with type \(type.rawValue)")
if type != .voIP { if type != .voIP {
completion() completion()
return return
} }
logger.debug("CallController: initializing chat") if AppChatState.shared.value == .stopped {
if (!ChatModel.shared.chatInitialized) { self.reportExpiredCall(payload: payload, completion)
initChatAndMigrate(refreshInvitations: false) return
} }
startChatAndActivate(dispatchQueue: DispatchQueue.global()) { if (!ChatModel.shared.chatInitialized) {
self.shouldSuspendChat = true logger.debug("CallController: initializing chat")
// There are no invitations in the model, as it was processed by NSE do {
_ = try? justRefreshCallInvitations() try initializeChat(start: true, refreshInvitations: false)
// logger.debug("CallController justRefreshCallInvitations: \(String(describing: m.callInvitations))") } catch let error {
// Extract the call information from the push notification payload logger.error("CallController: initializing chat error: \(error)")
let m = ChatModel.shared self.reportExpiredCall(payload: payload, completion)
if let contactId = payload.dictionaryPayload["contactId"] as? String, return
let invitation = m.callInvitations[contactId] { }
}
logger.debug("CallController: initialized chat")
startChatForCall()
logger.debug("CallController: started chat")
self.shouldSuspendChat = true
// There are no invitations in the model, as it was processed by NSE
_ = try? justRefreshCallInvitations()
logger.debug("CallController: updated call invitations chat")
// logger.debug("CallController justRefreshCallInvitations: \(String(describing: m.callInvitations))")
// Extract the call information from the push notification payload
let m = ChatModel.shared
if let contactId = payload.dictionaryPayload["contactId"] as? String,
let invitation = m.callInvitations[contactId] {
let update = self.cxCallUpdate(invitation: invitation)
if let uuid = invitation.callkitUUID {
logger.debug("CallController: report pushkit call via CallKit")
let update = self.cxCallUpdate(invitation: invitation) let update = self.cxCallUpdate(invitation: invitation)
if let uuid = invitation.callkitUUID { self.provider.reportNewIncomingCall(with: uuid, update: update) { error in
logger.debug("CallController: report pushkit call via CallKit") if error != nil {
let update = self.cxCallUpdate(invitation: invitation) m.callInvitations.removeValue(forKey: contactId)
self.provider.reportNewIncomingCall(with: uuid, update: update) { error in
if error != nil {
m.callInvitations.removeValue(forKey: contactId)
}
// Tell PushKit that the notification is handled.
completion()
} }
} else { // Tell PushKit that the notification is handled.
self.reportExpiredCall(update: update, completion) completion()
} }
} else { } else {
self.reportExpiredCall(payload: payload, completion) self.reportExpiredCall(update: update, completion)
} }
} else {
self.reportExpiredCall(payload: payload, completion)
} }
} }
@@ -211,7 +223,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
} }
func reportNewIncomingCall(invitation: RcvCallInvitation, completion: @escaping (Error?) -> Void) { func reportNewIncomingCall(invitation: RcvCallInvitation, completion: @escaping (Error?) -> Void) {
logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callkitUUID), privacy: .public)") logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callkitUUID))")
if CallController.useCallKit(), let uuid = invitation.callkitUUID { if CallController.useCallKit(), let uuid = invitation.callkitUUID {
if invitation.callTs.timeIntervalSinceNow >= -180 { if invitation.callTs.timeIntervalSinceNow >= -180 {
let update = cxCallUpdate(invitation: invitation) let update = cxCallUpdate(invitation: invitation)
@@ -351,7 +363,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
private func requestTransaction(with action: CXAction, onSuccess: @escaping () -> Void = {}) { private func requestTransaction(with action: CXAction, onSuccess: @escaping () -> Void = {}) {
controller.request(CXTransaction(action: action)) { error in controller.request(CXTransaction(action: action)) { error in
if let error = error { if let error = error {
logger.error("CallController.requestTransaction error requesting transaction: \(error.localizedDescription, privacy: .public)") logger.error("CallController.requestTransaction error requesting transaction: \(error.localizedDescription)")
} else { } else {
logger.debug("CallController.requestTransaction requested transaction successfully") logger.debug("CallController.requestTransaction requested transaction successfully")
onSuccess() onSuccess()
@@ -484,6 +484,7 @@ func deleteChatAsync() async throws {
try await apiDeleteStorage() try await apiDeleteStorage()
_ = kcDatabasePassword.remove() _ = kcDatabasePassword.remove()
storeDBPassphraseGroupDefault.set(true) storeDBPassphraseGroupDefault.set(true)
deleteAppDatabaseAndFiles()
} }
struct DatabaseView_Previews: PreviewProvider { struct DatabaseView_Previews: PreviewProvider {
@@ -13,19 +13,28 @@ struct LocalAuthView: View {
@EnvironmentObject var m: ChatModel @EnvironmentObject var m: ChatModel
var authRequest: LocalAuthRequest var authRequest: LocalAuthRequest
@State private var password = "" @State private var password = ""
@State private var allowToReact = true
var body: some View { var body: some View {
PasscodeView(passcode: $password, title: authRequest.title ?? "Enter Passcode", reason: authRequest.reason, submitLabel: "Submit") { PasscodeView(passcode: $password, title: authRequest.title ?? "Enter Passcode", reason: authRequest.reason, submitLabel: "Submit",
buttonsEnabled: $allowToReact) {
if let sdPassword = kcSelfDestructPassword.get(), authRequest.selfDestruct && password == sdPassword { if let sdPassword = kcSelfDestructPassword.get(), authRequest.selfDestruct && password == sdPassword {
allowToReact = false
deleteStorageAndRestart(sdPassword) { r in deleteStorageAndRestart(sdPassword) { r in
m.laRequest = nil m.laRequest = nil
authRequest.completed(r) authRequest.completed(r)
} }
return return
} }
let r: LAResult = password == authRequest.password let r: LAResult
? .success if password == authRequest.password {
: .failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry")) if authRequest.selfDestruct && kcSelfDestructPassword.get() != nil && !m.chatInitialized {
initChatAndMigrate()
}
r = .success
} else {
r = .failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry"))
}
m.laRequest = nil m.laRequest = nil
authRequest.completed(r) authRequest.completed(r)
} cancel: { } cancel: {
@@ -37,8 +46,27 @@ struct LocalAuthView: View {
private func deleteStorageAndRestart(_ password: String, completed: @escaping (LAResult) -> Void) { private func deleteStorageAndRestart(_ password: String, completed: @escaping (LAResult) -> Void) {
Task { Task {
do { do {
try await stopChatAsync() /** Waiting until [initializeChat] finishes */
try await deleteChatAsync() while (m.ctrlInitInProgress) {
try await Task.sleep(nanoseconds: 50_000000)
}
if m.chatRunning == true {
try await stopChatAsync()
}
if m.chatInitialized {
/**
* The following sequence can bring a user here:
* the user opened the app, entered app passcode, went to background, returned back, entered self-destruct code.
* In this case database should be closed to prevent possible situation when OS can deny database removal command
* */
chatCloseStore()
}
deleteAppDatabaseAndFiles()
// Clear sensitive data on screen just in case app fails to hide its views while new database is created
m.chatId = nil
m.reversedChatItems = []
m.chats = []
m.users = []
_ = kcAppPassword.set(password) _ = kcAppPassword.set(password)
_ = kcSelfDestructPassword.remove() _ = kcSelfDestructPassword.remove()
await NtfManager.shared.removeAllNotifications() await NtfManager.shared.removeAllNotifications()
@@ -53,7 +81,7 @@ struct LocalAuthView: View {
try initializeChat(start: true) try initializeChat(start: true)
m.chatDbChanged = false m.chatDbChanged = false
AppChatState.shared.set(.active) AppChatState.shared.set(.active)
if m.currentUser != nil { return } if m.currentUser != nil || !m.chatInitialized { return }
var profile: Profile? = nil var profile: Profile? = nil
if let displayName = displayName, displayName != "" { if let displayName = displayName, displayName != "" {
profile = Profile(displayName: displayName, fullName: "") profile = Profile(displayName: displayName, fullName: "")
@@ -14,6 +14,8 @@ struct PasscodeView: View {
var reason: String? = nil var reason: String? = nil
var submitLabel: LocalizedStringKey var submitLabel: LocalizedStringKey
var submitEnabled: ((String) -> Bool)? var submitEnabled: ((String) -> Bool)?
@Binding var buttonsEnabled: Bool
var submit: () -> Void var submit: () -> Void
var cancel: () -> Void var cancel: () -> Void
@@ -70,11 +72,11 @@ struct PasscodeView: View {
@ViewBuilder private func buttonsView() -> some View { @ViewBuilder private func buttonsView() -> some View {
Button(action: cancel) { Button(action: cancel) {
Label("Cancel", systemImage: "multiply") Label("Cancel", systemImage: "multiply")
} }.disabled(!buttonsEnabled)
Button(action: submit) { Button(action: submit) {
Label(submitLabel, systemImage: "checkmark") Label(submitLabel, systemImage: "checkmark")
} }
.disabled(submitEnabled?(passcode) == false || passcode.count < 4) .disabled(submitEnabled?(passcode) == false || passcode.count < 4 || !buttonsEnabled)
} }
} }
@@ -85,6 +87,7 @@ struct PasscodeViewView_Previews: PreviewProvider {
title: "Enter Passcode", title: "Enter Passcode",
reason: "Unlock app", reason: "Unlock app",
submitLabel: "Submit", submitLabel: "Submit",
buttonsEnabled: Binding.constant(true),
submit: {}, submit: {},
cancel: {} cancel: {}
) )
@@ -11,6 +11,7 @@ import SimpleXChat
struct SetAppPasscodeView: View { struct SetAppPasscodeView: View {
var passcodeKeychain: KeyChainItem = kcAppPassword var passcodeKeychain: KeyChainItem = kcAppPassword
var prohibitedPasscodeKeychain: KeyChainItem = kcSelfDestructPassword
var title: LocalizedStringKey = "New Passcode" var title: LocalizedStringKey = "New Passcode"
var reason: String? var reason: String?
var submit: () -> Void var submit: () -> Void
@@ -41,7 +42,10 @@ struct SetAppPasscodeView: View {
} }
} }
} else { } else {
setPasswordView(title: title, submitLabel: "Save") { setPasswordView(title: title,
submitLabel: "Save",
// Do not allow to set app passcode == selfDestruct passcode
submitEnabled: { pwd in pwd != prohibitedPasscodeKeychain.get() }) {
enteredPassword = passcode enteredPassword = passcode
passcode = "" passcode = ""
confirming = true confirming = true
@@ -54,7 +58,7 @@ struct SetAppPasscodeView: View {
} }
private func setPasswordView(title: LocalizedStringKey, submitLabel: LocalizedStringKey, submitEnabled: (((String) -> Bool))? = nil, submit: @escaping () -> Void) -> some View { private func setPasswordView(title: LocalizedStringKey, submitLabel: LocalizedStringKey, submitEnabled: (((String) -> Bool))? = nil, submit: @escaping () -> Void) -> some View {
PasscodeView(passcode: $passcode, title: title, reason: reason, submitLabel: submitLabel, submitEnabled: submitEnabled, submit: submit) { PasscodeView(passcode: $passcode, title: title, reason: reason, submitLabel: submitLabel, submitEnabled: submitEnabled, buttonsEnabled: Binding.constant(true), submit: submit) {
dismiss() dismiss()
cancel() cancel()
} }
@@ -11,12 +11,14 @@ import SimpleXChat
enum UserProfileAlert: Identifiable { enum UserProfileAlert: Identifiable {
case duplicateUserError case duplicateUserError
case invalidDisplayNameError
case createUserError(error: LocalizedStringKey) case createUserError(error: LocalizedStringKey)
case invalidNameError(validName: String) case invalidNameError(validName: String)
var id: String { var id: String {
switch self { switch self {
case .duplicateUserError: return "duplicateUserError" case .duplicateUserError: return "duplicateUserError"
case .invalidDisplayNameError: return "invalidDisplayNameError"
case .createUserError: return "createUserError" case .createUserError: return "createUserError"
case let .invalidNameError(validName): return "invalidNameError \(validName)" case let .invalidNameError(validName): return "invalidNameError \(validName)"
} }
@@ -187,6 +189,12 @@ private func createProfile(_ displayName: String, showAlert: (UserProfileAlert)
} else { } else {
showAlert(.duplicateUserError) showAlert(.duplicateUserError)
} }
case .chatCmdError(_, .error(.invalidDisplayName)):
if m.currentUser == nil {
AlertManager.shared.showAlert(invalidDisplayNameAlert)
} else {
showAlert(.invalidDisplayNameError)
}
default: default:
let err: LocalizedStringKey = "Error: \(responseError(error))" let err: LocalizedStringKey = "Error: \(responseError(error))"
if m.currentUser == nil { if m.currentUser == nil {
@@ -207,6 +215,7 @@ private func canCreateProfile(_ displayName: String) -> Bool {
func userProfileAlert(_ alert: UserProfileAlert, _ displayName: Binding<String>) -> Alert { func userProfileAlert(_ alert: UserProfileAlert, _ displayName: Binding<String>) -> Alert {
switch alert { switch alert {
case .duplicateUserError: return duplicateUserAlert case .duplicateUserError: return duplicateUserAlert
case .invalidDisplayNameError: return invalidDisplayNameAlert
case let .createUserError(err): return creatUserErrorAlert(err) case let .createUserError(err): return creatUserErrorAlert(err)
case let .invalidNameError(name): return createInvalidNameAlert(name, displayName) case let .invalidNameError(name): return createInvalidNameAlert(name, displayName)
} }
@@ -219,6 +228,13 @@ private var duplicateUserAlert: Alert {
) )
} }
private var invalidDisplayNameAlert: Alert {
Alert(
title: Text("Invalid display name!"),
message: Text("This display name is invalid. Please choose another name.")
)
}
private func creatUserErrorAlert(_ err: LocalizedStringKey) -> Alert { private func creatUserErrorAlert(_ err: LocalizedStringKey) -> Alert {
Alert( Alert(
title: Text("Error creating profile!"), title: Text("Error creating profile!"),
@@ -491,14 +491,23 @@ struct SimplexLockView: View {
showLAAlert(.laPasscodeNotChangedAlert) showLAAlert(.laPasscodeNotChangedAlert)
} }
case .enableSelfDestruct: case .enableSelfDestruct:
SetAppPasscodeView(passcodeKeychain: kcSelfDestructPassword, title: "Set passcode", reason: NSLocalizedString("Enable self-destruct passcode", comment: "set passcode view")) { SetAppPasscodeView(
passcodeKeychain: kcSelfDestructPassword,
prohibitedPasscodeKeychain: kcAppPassword,
title: "Set passcode",
reason: NSLocalizedString("Enable self-destruct passcode", comment: "set passcode view")
) {
updateSelfDestruct() updateSelfDestruct()
showLAAlert(.laSelfDestructPasscodeSetAlert) showLAAlert(.laSelfDestructPasscodeSetAlert)
} cancel: { } cancel: {
revertSelfDestruct() revertSelfDestruct()
} }
case .changeSelfDestructPasscode: case .changeSelfDestructPasscode:
SetAppPasscodeView(passcodeKeychain: kcSelfDestructPassword, reason: NSLocalizedString("Change self-destruct passcode", comment: "set passcode view")) { SetAppPasscodeView(
passcodeKeychain: kcSelfDestructPassword,
prohibitedPasscodeKeychain: kcAppPassword,
reason: NSLocalizedString("Change self-destruct passcode", comment: "set passcode view")
) {
showLAAlert(.laSelfDestructPasscodeChangedAlert) showLAAlert(.laSelfDestructPasscodeChangedAlert)
} cancel: { } cancel: {
showLAAlert(.laPasscodeNotChangedAlert) showLAAlert(.laPasscodeNotChangedAlert)
+100 -46
View File
@@ -16,9 +16,11 @@ let logger = Logger()
let appSuspendingDelay: UInt64 = 2_500_000_000 let appSuspendingDelay: UInt64 = 2_500_000_000
let nseSuspendDelay: TimeInterval = 2 typealias SuspendSchedule = (delay: TimeInterval, timeout: Int)
let nseSuspendTimeout: Int = 5 let nseSuspendSchedule: SuspendSchedule = (2, 4)
let fastNSESuspendSchedule: SuspendSchedule = (1, 1)
typealias NtfStream = ConcurrentQueue<NSENotification> typealias NtfStream = ConcurrentQueue<NSENotification>
@@ -32,7 +34,7 @@ actor PendingNtfs {
private var ntfStreams: [String: NtfStream] = [:] private var ntfStreams: [String: NtfStream] = [:]
func createStream(_ id: String) async { func createStream(_ id: String) async {
logger.debug("NotificationService PendingNtfs.createStream: \(id, privacy: .public)") logger.debug("NotificationService PendingNtfs.createStream: \(id)")
if ntfStreams[id] == nil { if ntfStreams[id] == nil {
ntfStreams[id] = ConcurrentQueue() ntfStreams[id] = ConcurrentQueue()
logger.debug("NotificationService PendingNtfs.createStream: created ConcurrentQueue") logger.debug("NotificationService PendingNtfs.createStream: created ConcurrentQueue")
@@ -40,14 +42,14 @@ actor PendingNtfs {
} }
func readStream(_ id: String, for nse: NotificationService, ntfInfo: NtfMessages) async { func readStream(_ id: String, for nse: NotificationService, ntfInfo: NtfMessages) async {
logger.debug("NotificationService PendingNtfs.readStream: \(id, privacy: .public) \(ntfInfo.ntfMessages.count, privacy: .public)") logger.debug("NotificationService PendingNtfs.readStream: \(id) \(ntfInfo.ntfMessages.count)")
if !ntfInfo.user.showNotifications { if !ntfInfo.user.showNotifications {
nse.setBestAttemptNtf(.empty) nse.setBestAttemptNtf(.empty)
} }
if let s = ntfStreams[id] { if let s = ntfStreams[id] {
logger.debug("NotificationService PendingNtfs.readStream: has stream") logger.debug("NotificationService PendingNtfs.readStream: has stream")
var expected = Set(ntfInfo.ntfMessages.map { $0.msgId }) var expected = Set(ntfInfo.ntfMessages.map { $0.msgId })
logger.debug("NotificationService PendingNtfs.readStream: expecting: \(expected, privacy: .public)") logger.debug("NotificationService PendingNtfs.readStream: expecting: \(expected)")
var readCancelled = false var readCancelled = false
var dequeued: DequeueElement<NSENotification>? var dequeued: DequeueElement<NSENotification>?
nse.cancelRead = { nse.cancelRead = {
@@ -66,7 +68,7 @@ actor PendingNtfs {
} else if case let .msgInfo(info) = ntf { } else if case let .msgInfo(info) = ntf {
let found = expected.remove(info.msgId) let found = expected.remove(info.msgId)
if found != nil { if found != nil {
logger.debug("NotificationService PendingNtfs.readStream: msgInfo, last: \(expected.isEmpty, privacy: .public)") logger.debug("NotificationService PendingNtfs.readStream: msgInfo, last: \(expected.isEmpty)")
if expected.isEmpty { break } if expected.isEmpty { break }
} else if let msgTs = ntfInfo.msgTs, info.msgTs > msgTs { } else if let msgTs = ntfInfo.msgTs, info.msgTs > msgTs {
logger.debug("NotificationService PendingNtfs.readStream: unexpected msgInfo") logger.debug("NotificationService PendingNtfs.readStream: unexpected msgInfo")
@@ -88,7 +90,7 @@ actor PendingNtfs {
} }
func writeStream(_ id: String, _ ntf: NSENotification) async { func writeStream(_ id: String, _ ntf: NSENotification) async {
logger.debug("NotificationService PendingNtfs.writeStream: \(id, privacy: .public)") logger.debug("NotificationService PendingNtfs.writeStream: \(id)")
if let s = ntfStreams[id] { if let s = ntfStreams[id] {
logger.debug("NotificationService PendingNtfs.writeStream: writing ntf") logger.debug("NotificationService PendingNtfs.writeStream: writing ntf")
s.enqueue(ntf) s.enqueue(ntf)
@@ -208,7 +210,7 @@ class NotificationService: UNNotificationServiceExtension {
self.contentHandler = contentHandler self.contentHandler = contentHandler
registerGroupDefaults() registerGroupDefaults()
let appState = appStateGroupDefault.get() let appState = appStateGroupDefault.get()
logger.debug("NotificationService: app is \(appState.rawValue, privacy: .public)") logger.debug("NotificationService: app is \(appState.rawValue)")
switch appState { switch appState {
case .stopped: case .stopped:
setBadgeCount() setBadgeCount()
@@ -238,7 +240,7 @@ class NotificationService: UNNotificationServiceExtension {
} }
} }
} }
logger.debug("NotificationService: app state is now \(state.rawValue, privacy: .public)") logger.debug("NotificationService: app state is now \(state.rawValue)")
if state.inactive { if state.inactive {
receiveNtfMessages(request, contentHandler) receiveNtfMessages(request, contentHandler)
} else { } else {
@@ -267,7 +269,7 @@ class NotificationService: UNNotificationServiceExtension {
let dbStatus = startChat() let dbStatus = startChat()
if case .ok = dbStatus, if case .ok = dbStatus,
let ntfInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) { let ntfInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) {
logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo.ntfMessages.count), privacy: .public)") logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo.ntfMessages.count))")
if let connEntity = ntfInfo.connEntity_ { if let connEntity = ntfInfo.connEntity_ {
setBestAttemptNtf( setBestAttemptNtf(
ntfInfo.ntfsEnabled ntfInfo.ntfsEnabled
@@ -279,7 +281,7 @@ class NotificationService: UNNotificationServiceExtension {
NtfStreamSemaphores.shared.waitForStream(id) NtfStreamSemaphores.shared.waitForStream(id)
if receiveEntityId != nil { if receiveEntityId != nil {
Task { Task {
logger.debug("NotificationService: receiveNtfMessages: in Task, connEntity id \(id, privacy: .public)") logger.debug("NotificationService: receiveNtfMessages: in Task, connEntity id \(id)")
await PendingNtfs.shared.createStream(id) await PendingNtfs.shared.createStream(id)
await PendingNtfs.shared.readStream(id, for: self, ntfInfo: ntfInfo) await PendingNtfs.shared.readStream(id, for: self, ntfInfo: ntfInfo)
deliverBestAttemptNtf() deliverBestAttemptNtf()
@@ -297,7 +299,7 @@ class NotificationService: UNNotificationServiceExtension {
override func serviceExtensionTimeWillExpire() { override func serviceExtensionTimeWillExpire() {
logger.debug("DEBUGGING: NotificationService.serviceExtensionTimeWillExpire") logger.debug("DEBUGGING: NotificationService.serviceExtensionTimeWillExpire")
deliverBestAttemptNtf() deliverBestAttemptNtf(urgent: true)
} }
func setBadgeCount() { func setBadgeCount() {
@@ -319,7 +321,7 @@ class NotificationService: UNNotificationServiceExtension {
} }
} }
private func deliverBestAttemptNtf() { private func deliverBestAttemptNtf(urgent: Bool = false) {
logger.debug("NotificationService.deliverBestAttemptNtf") logger.debug("NotificationService.deliverBestAttemptNtf")
if let cancel = cancelRead { if let cancel = cancelRead {
cancelRead = nil cancelRead = nil
@@ -329,20 +331,55 @@ class NotificationService: UNNotificationServiceExtension {
receiveEntityId = nil receiveEntityId = nil
NtfStreamSemaphores.shared.signalStreamReady(id) NtfStreamSemaphores.shared.signalStreamReady(id)
} }
let suspend: Bool
if let t = threadId { if let t = threadId {
threadId = nil threadId = nil
if NSEThreads.shared.endThread(t) { suspend = NSEThreads.shared.endThread(t) && NSEThreads.shared.noThreads
logger.debug("NotificationService.deliverBestAttemptNtf: will suspend") } else {
// suspension is delayed to allow chat core finalise any processing suspend = false
// (e.g., send delivery receipts) }
DispatchQueue.global().asyncAfter(deadline: .now() + nseSuspendDelay) { deliverCallkitOrNotification(urgent: urgent, suspend: suspend)
if NSEThreads.shared.noThreads { }
logger.debug("NotificationService.deliverBestAttemptNtf: suspending...")
suspendChat(nseSuspendTimeout) private func deliverCallkitOrNotification(urgent: Bool, suspend: Bool = false) {
if case .callkit = bestAttemptNtf {
logger.debug("NotificationService.deliverCallkitOrNotification: will suspend, callkit")
if urgent {
// suspending NSE even though there may be other notifications
// to allow the app to process callkit call
suspendChat(0)
deliverNotification()
} else {
// suspending NSE with delay and delivering after the suspension
// because pushkit notification must be processed without delay
// to avoid app termination
DispatchQueue.global().asyncAfter(deadline: .now() + fastNSESuspendSchedule.delay) {
suspendChat(fastNSESuspendSchedule.timeout)
DispatchQueue.global().asyncAfter(deadline: .now() + Double(fastNSESuspendSchedule.timeout)) {
self.deliverNotification()
} }
} }
} }
} else {
if suspend {
logger.debug("NotificationService.deliverCallkitOrNotification: will suspend")
if urgent {
suspendChat(0)
} else {
// suspension is delayed to allow chat core finalise any processing
// (e.g., send delivery receipts)
DispatchQueue.global().asyncAfter(deadline: .now() + nseSuspendSchedule.delay) {
if NSEThreads.shared.noThreads {
suspendChat(nseSuspendSchedule.timeout)
}
}
}
}
deliverNotification()
} }
}
private func deliverNotification() {
if let handler = contentHandler, let ntf = bestAttemptNtf { if let handler = contentHandler, let ntf = bestAttemptNtf {
contentHandler = nil contentHandler = nil
bestAttemptNtf = nil bestAttemptNtf = nil
@@ -357,17 +394,14 @@ class NotificationService: UNNotificationServiceExtension {
switch ntf { switch ntf {
case let .nse(content): deliver(content) case let .nse(content): deliver(content)
case let .callkit(invitation): case let .callkit(invitation):
logger.debug("NotificationService reportNewIncomingVoIPPushPayload for \(invitation.contact.id)")
CXProvider.reportNewIncomingVoIPPushPayload([ CXProvider.reportNewIncomingVoIPPushPayload([
"displayName": invitation.contact.displayName, "displayName": invitation.contact.displayName,
"contactId": invitation.contact.id, "contactId": invitation.contact.id,
"media": invitation.callType.media.rawValue "media": invitation.callType.media.rawValue
]) { error in ]) { error in
if error == nil { logger.debug("reportNewIncomingVoIPPushPayload result: \(error)")
deliver(nil) deliver(error == nil ? nil : createCallInvitationNtf(invitation))
} else {
logger.debug("NotificationService reportNewIncomingVoIPPushPayload success to CallController for \(invitation.contact.id)")
deliver(createCallInvitationNtf(invitation))
}
} }
case .empty: deliver(nil) // used to mute notifications that did not unsubscribe yet case .empty: deliver(nil) // used to mute notifications that did not unsubscribe yet
case .msgInfo: deliver(nil) // unreachable, the best attempt is never set to msgInfo case .msgInfo: deliver(nil) // unreachable, the best attempt is never set to msgInfo
@@ -402,14 +436,14 @@ var appSubscriber: AppSubscriber = appStateSubscriber { state in
logger.debug("NotificationService: appSubscriber") logger.debug("NotificationService: appSubscriber")
if state.running && NSEChatState.shared.value.canSuspend { if state.running && NSEChatState.shared.value.canSuspend {
logger.debug("NotificationService: appSubscriber app state \(state.rawValue), suspending") logger.debug("NotificationService: appSubscriber app state \(state.rawValue), suspending")
suspendChat(nseSuspendTimeout) suspendChat(fastNSESuspendSchedule.timeout)
} }
} }
func appStateSubscriber(onState: @escaping (AppState) -> Void) -> AppSubscriber { func appStateSubscriber(onState: @escaping (AppState) -> Void) -> AppSubscriber {
appMessageSubscriber { msg in appMessageSubscriber { msg in
if case let .state(state) = msg { if case let .state(state) = msg {
logger.debug("NotificationService: appStateSubscriber \(state.rawValue, privacy: .public)") logger.debug("NotificationService: appStateSubscriber \(state.rawValue)")
onState(state) onState(state)
} }
} }
@@ -425,24 +459,33 @@ let xftpConfig: XFTPFileConfig? = getXFTPCfg()
// Subsequent calls to didReceive will be waiting on semaphore and won't start chat again, as it will be .active // Subsequent calls to didReceive will be waiting on semaphore and won't start chat again, as it will be .active
func startChat() -> DBMigrationResult? { func startChat() -> DBMigrationResult? {
logger.debug("NotificationService: startChat") logger.debug("NotificationService: startChat")
if case .active = NSEChatState.shared.value { return .ok } // only skip creating if there is chat controller
if case .active = NSEChatState.shared.value, hasChatCtrl() { return .ok }
startLock.wait() startLock.wait()
defer { startLock.signal() } defer { startLock.signal() }
return switch NSEChatState.shared.value { if hasChatCtrl() {
case .created: doStartChat() return switch NSEChatState.shared.value {
case .starting: .ok // it should never get to this branch, as it would be waiting for start on startLock case .created: doStartChat()
case .active: .ok case .starting: .ok // it should never get to this branch, as it would be waiting for start on startLock
case .suspending: activateChat() case .active: .ok
case .suspended: activateChat() case .suspending: activateChat()
case .suspended: activateChat()
}
} else {
// Ignore state in preference if there is no chat controller.
// State in preference may have failed to update e.g. because of a crash.
NSEChatState.shared.set(.created)
return doStartChat()
} }
} }
func doStartChat() -> DBMigrationResult? { func doStartChat() -> DBMigrationResult? {
logger.debug("NotificationService: doStartChat") logger.debug("NotificationService: doStartChat")
hs_init(0, nil) haskell_init_nse()
let (_, dbStatus) = chatMigrateInit(confirmMigrations: defaultMigrationConfirmation(), backgroundMode: true) let (_, dbStatus) = chatMigrateInit(confirmMigrations: defaultMigrationConfirmation(), backgroundMode: true)
logger.debug("NotificationService: doStartChat \(String(describing: dbStatus))")
if dbStatus != .ok { if dbStatus != .ok {
resetChatCtrl() resetChatCtrl()
NSEChatState.shared.set(.created) NSEChatState.shared.set(.created)
@@ -477,7 +520,7 @@ func doStartChat() -> DBMigrationResult? {
return .ok return .ok
} }
} catch { } catch {
logger.error("NotificationService startChat error: \(responseError(error), privacy: .public)") logger.error("NotificationService startChat error: \(responseError(error))")
} }
} else { } else {
logger.debug("NotificationService: no active user") logger.debug("NotificationService: no active user")
@@ -504,8 +547,10 @@ func suspendChat(_ timeout: Int) {
logger.debug("NotificationService: suspendChat") logger.debug("NotificationService: suspendChat")
let state = NSEChatState.shared.value let state = NSEChatState.shared.value
if !state.canSuspend { if !state.canSuspend {
logger.error("NotificationService suspendChat called, current state: \(state.rawValue, privacy: .public)") logger.error("NotificationService suspendChat called, current state: \(state.rawValue)")
} else { } else if hasChatCtrl() {
// only suspend if we have chat controller to avoid crashes when suspension is
// attempted when chat controller was not created
suspendLock.wait() suspendLock.wait()
defer { suspendLock.signal() } defer { suspendLock.signal() }
@@ -571,7 +616,7 @@ private let isInChina = SKStorefront().countryCode == "CHN"
private func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() } private func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() }
func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? { func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
logger.debug("NotificationService receivedMsgNtf: \(res.responseType, privacy: .public)") logger.debug("NotificationService receivedMsgNtf: \(res.responseType)")
switch res { switch res {
case let .contactConnected(user, contact, _): case let .contactConnected(user, contact, _):
return (contact.id, .nse(createContactConnectedNtf(user, contact))) return (contact.id, .nse(createContactConnectedNtf(user, contact)))
@@ -613,6 +658,9 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
case .chatSuspended: case .chatSuspended:
chatSuspended() chatSuspended()
return nil return nil
case let .chatError(_, err):
logger.error("NotificationService receivedMsgNtf error: \(String(describing: err))")
return nil
default: default:
logger.debug("NotificationService receivedMsgNtf ignored event: \(res.responseType)") logger.debug("NotificationService receivedMsgNtf ignored event: \(res.responseType)")
return nil return nil
@@ -627,17 +675,22 @@ func updateNetCfg() {
try setNetworkConfig(networkConfig) try setNetworkConfig(networkConfig)
networkConfig = newNetConfig networkConfig = newNetConfig
} catch { } catch {
logger.error("NotificationService apply changed network config error: \(responseError(error), privacy: .public)") logger.error("NotificationService apply changed network config error: \(responseError(error))")
} }
} }
} }
func apiGetActiveUser() -> User? { func apiGetActiveUser() -> User? {
let r = sendSimpleXCmd(.showActiveUser) let r = sendSimpleXCmd(.showActiveUser)
logger.debug("apiGetActiveUser sendSimpleXCmd response: \(String(describing: r))") logger.debug("apiGetActiveUser sendSimpleXCmd response: \(r.responseType)")
switch r { switch r {
case let .activeUser(user): return user case let .activeUser(user): return user
case .chatCmdError(_, .error(.noActiveUser)): return nil case .chatCmdError(_, .error(.noActiveUser)):
logger.debug("apiGetActiveUser sendSimpleXCmd no active user")
return nil
case let .chatCmdError(_, err):
logger.debug("apiGetActiveUser sendSimpleXCmd error: \(String(describing: err))")
return nil
default: default:
logger.error("NotificationService apiGetActiveUser unexpected response: \(String(describing: r))") logger.error("NotificationService apiGetActiveUser unexpected response: \(String(describing: r))")
return nil return nil
@@ -645,7 +698,7 @@ func apiGetActiveUser() -> User? {
} }
func apiStartChat() throws -> Bool { func apiStartChat() throws -> Bool {
let r = sendSimpleXCmd(.startChat(subscribe: false, expire: false, xftp: false)) let r = sendSimpleXCmd(.startChat(mainApp: false))
switch r { switch r {
case .chatStarted: return true case .chatStarted: return true
case .chatRunning: return false case .chatRunning: return false
@@ -699,11 +752,12 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
} }
let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo)) let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo))
if case let .ntfMessages(user, connEntity_, msgTs, ntfMessages) = r, let user = user { if case let .ntfMessages(user, connEntity_, msgTs, ntfMessages) = r, let user = user {
logger.debug("apiGetNtfMessage response ntfMessages: \(ntfMessages.count)")
return NtfMessages(user: user, connEntity_: connEntity_, msgTs: msgTs, ntfMessages: ntfMessages) return NtfMessages(user: user, connEntity_: connEntity_, msgTs: msgTs, ntfMessages: ntfMessages)
} else if case let .chatCmdError(_, error) = r { } else if case let .chatCmdError(_, error) = r {
logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))") logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))")
} else { } else {
logger.debug("apiGetNtfMessage ignored response: \(r.responseType, privacy: .public) \(String.init(describing: r), privacy: .private)") logger.debug("apiGetNtfMessage ignored response: \(r.responseType) \(String.init(describing: r))")
} }
return nil return nil
} }
+20 -20
View File
@@ -29,6 +29,11 @@
5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; }; 5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; };
5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C13730A28156D2700F43030 /* ContactConnectionView.swift */; }; 5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C13730A28156D2700F43030 /* ContactConnectionView.swift */; };
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */; }; 5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */; };
5C245F192B4DB982001CC39F /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C245F142B4DB982001CC39F /* libgmpxx.a */; };
5C245F1A2B4DB982001CC39F /* libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C245F152B4DB982001CC39F /* libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl-ghc9.6.3.a */; };
5C245F1B2B4DB982001CC39F /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C245F162B4DB982001CC39F /* libgmp.a */; };
5C245F1C2B4DB982001CC39F /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C245F172B4DB982001CC39F /* libffi.a */; };
5C245F1D2B4DB982001CC39F /* libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C245F182B4DB982001CC39F /* libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl.a */; };
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */; }; 5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */; };
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260A27A30CFA00F70299 /* ChatListView.swift */; }; 5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260A27A30CFA00F70299 /* ChatListView.swift */; };
5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260E27A30FDC00F70299 /* ChatView.swift */; }; 5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260E27A30FDC00F70299 /* ChatView.swift */; };
@@ -42,11 +47,6 @@
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */; }; 5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */; };
5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */; }; 5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */; };
5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B3B09285FB130003915F2 /* DatabaseView.swift */; }; 5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B3B09285FB130003915F2 /* DatabaseView.swift */; };
5C4E80E42B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80DF2B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ.a */; };
5C4E80E52B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80E02B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ-ghc9.6.3.a */; };
5C4E80E62B40A96C0080FAE2 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80E12B40A96C0080FAE2 /* libgmp.a */; };
5C4E80E72B40A96C0080FAE2 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80E22B40A96C0080FAE2 /* libgmpxx.a */; };
5C4E80E82B40A96C0080FAE2 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80E32B40A96C0080FAE2 /* libffi.a */; };
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; }; 5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; };
5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A91E283AD0E400C4E99E /* CallManager.swift */; }; 5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A91E283AD0E400C4E99E /* CallManager.swift */; };
5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */; }; 5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */; };
@@ -275,6 +275,11 @@
5C13730A28156D2700F43030 /* ContactConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionView.swift; sourceTree = "<group>"; }; 5C13730A28156D2700F43030 /* ContactConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionView.swift; sourceTree = "<group>"; };
5C13730C2815740A00F43030 /* DebugJSON.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = DebugJSON.playground; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 5C13730C2815740A00F43030 /* DebugJSON.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = DebugJSON.playground; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemView.swift; sourceTree = "<group>"; }; 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemView.swift; sourceTree = "<group>"; };
5C245F142B4DB982001CC39F /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C245F152B4DB982001CC39F /* libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl-ghc9.6.3.a"; sourceTree = "<group>"; };
5C245F162B4DB982001CC39F /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C245F172B4DB982001CC39F /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C245F182B4DB982001CC39F /* libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl.a"; sourceTree = "<group>"; };
5C2E260627A2941F00F70299 /* SimpleXAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXAPI.swift; sourceTree = "<group>"; }; 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXAPI.swift; sourceTree = "<group>"; };
5C2E260A27A30CFA00F70299 /* ChatListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListView.swift; sourceTree = "<group>"; }; 5C2E260A27A30CFA00F70299 /* ChatListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListView.swift; sourceTree = "<group>"; };
5C2E260E27A30FDC00F70299 /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; }; 5C2E260E27A30FDC00F70299 /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; };
@@ -289,11 +294,6 @@
5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettings.swift; sourceTree = "<group>"; }; 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettings.swift; sourceTree = "<group>"; };
5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = "<group>"; }; 5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = "<group>"; };
5C4B3B09285FB130003915F2 /* DatabaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseView.swift; sourceTree = "<group>"; }; 5C4B3B09285FB130003915F2 /* DatabaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseView.swift; sourceTree = "<group>"; };
5C4E80DF2B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ.a"; sourceTree = "<group>"; };
5C4E80E02B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ-ghc9.6.3.a"; sourceTree = "<group>"; };
5C4E80E12B40A96C0080FAE2 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C4E80E22B40A96C0080FAE2 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C4E80E32B40A96C0080FAE2 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = "<group>"; }; 5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = "<group>"; };
5C55A91E283AD0E400C4E99E /* CallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = "<group>"; }; 5C55A91E283AD0E400C4E99E /* CallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = "<group>"; };
5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = "<group>"; }; 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = "<group>"; };
@@ -511,13 +511,13 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
5C4E80E72B40A96C0080FAE2 /* libgmpxx.a in Frameworks */, 5C245F192B4DB982001CC39F /* libgmpxx.a in Frameworks */,
5C245F1C2B4DB982001CC39F /* libffi.a in Frameworks */,
5C245F1D2B4DB982001CC39F /* libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl.a in Frameworks */,
5C245F1B2B4DB982001CC39F /* libgmp.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5C4E80E62B40A96C0080FAE2 /* libgmp.a in Frameworks */, 5C245F1A2B4DB982001CC39F /* libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl-ghc9.6.3.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
5C4E80E82B40A96C0080FAE2 /* libffi.a in Frameworks */,
5C4E80E52B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ-ghc9.6.3.a in Frameworks */,
5C4E80E42B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -579,11 +579,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = { 5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5C4E80E32B40A96C0080FAE2 /* libffi.a */, 5C245F172B4DB982001CC39F /* libffi.a */,
5C4E80E12B40A96C0080FAE2 /* libgmp.a */, 5C245F162B4DB982001CC39F /* libgmp.a */,
5C4E80E22B40A96C0080FAE2 /* libgmpxx.a */, 5C245F142B4DB982001CC39F /* libgmpxx.a */,
5C4E80E02B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ-ghc9.6.3.a */, 5C245F152B4DB982001CC39F /* libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl-ghc9.6.3.a */,
5C4E80DF2B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ.a */, 5C245F182B4DB982001CC39F /* libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl.a */,
); );
path = Libraries; path = Libraries;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -41,7 +41,7 @@
</Testables> </Testables>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Release" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0" launchStyle = "0"
@@ -2,7 +2,7 @@
<Scheme <Scheme
LastUpgradeVersion = "1400" LastUpgradeVersion = "1400"
wasCreatedForAppExtension = "YES" wasCreatedForAppExtension = "YES"
version = "2.0"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"> buildImplicitDependencies = "YES">
@@ -47,16 +47,14 @@
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0" launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO" ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES" debugDocumentVersioning = "YES"
debugServiceExtension = "internal" debugServiceExtension = "internal"
allowLocationSimulation = "YES" allowLocationSimulation = "YES">
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable <BuildableProductRunnable
runnableDebuggingMode = "0"> runnableDebuggingMode = "0">
<BuildableReference <BuildableReference
+5 -1
View File
@@ -12,7 +12,11 @@ private var chatController: chat_ctrl?
private var migrationResult: (Bool, DBMigrationResult)? private var migrationResult: (Bool, DBMigrationResult)?
public func getChatCtrl(_ useKey: String? = nil) -> chat_ctrl { public func hasChatCtrl() -> Bool {
chatController != nil
}
public func getChatCtrl() -> chat_ctrl {
if let controller = chatController { return controller } if let controller = chatController { return controller }
fatalError("chat controller not initialized") fatalError("chat controller not initialized")
} }
+3 -2
View File
@@ -25,7 +25,7 @@ public enum ChatCommand {
case apiMuteUser(userId: Int64) case apiMuteUser(userId: Int64)
case apiUnmuteUser(userId: Int64) case apiUnmuteUser(userId: Int64)
case apiDeleteUser(userId: Int64, delSMPQueues: Bool, viewPwd: String?) case apiDeleteUser(userId: Int64, delSMPQueues: Bool, viewPwd: String?)
case startChat(subscribe: Bool, expire: Bool, xftp: Bool) case startChat(mainApp: Bool)
case apiStopChat case apiStopChat
case apiActivateChat(restoreChat: Bool) case apiActivateChat(restoreChat: Bool)
case apiSuspendChat(timeoutMicroseconds: Int) case apiSuspendChat(timeoutMicroseconds: Int)
@@ -154,7 +154,7 @@ public enum ChatCommand {
case let .apiMuteUser(userId): return "/_mute user \(userId)" case let .apiMuteUser(userId): return "/_mute user \(userId)"
case let .apiUnmuteUser(userId): return "/_unmute user \(userId)" case let .apiUnmuteUser(userId): return "/_unmute user \(userId)"
case let .apiDeleteUser(userId, delSMPQueues, viewPwd): return "/_delete user \(userId) del_smp=\(onOff(delSMPQueues))\(maybePwd(viewPwd))" case let .apiDeleteUser(userId, delSMPQueues, viewPwd): return "/_delete user \(userId) del_smp=\(onOff(delSMPQueues))\(maybePwd(viewPwd))"
case let .startChat(subscribe, expire, xftp): return "/_start subscribe=\(onOff(subscribe)) expire=\(onOff(expire)) xftp=\(onOff(xftp))" case let .startChat(mainApp): return "/_start main=\(onOff(mainApp))"
case .apiStopChat: return "/_stop" case .apiStopChat: return "/_stop"
case let .apiActivateChat(restore): return "/_app activate restore=\(onOff(restore))" case let .apiActivateChat(restore): return "/_app activate restore=\(onOff(restore))"
case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)" case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)"
@@ -1610,6 +1610,7 @@ public enum ChatErrorType: Decodable {
case userUnknown case userUnknown
case activeUserExists case activeUserExists
case userExists case userExists
case invalidDisplayName
case differentActiveUser(commandUserId: Int64, activeUserId: Int64) case differentActiveUser(commandUserId: Int64, activeUserId: Int64)
case cantDeleteActiveUser(userId: Int64) case cantDeleteActiveUser(userId: Int64)
case cantDeleteLastUser(userId: Int64) case cantDeleteLastUser(userId: Int64)
-1
View File
@@ -172,7 +172,6 @@ public func fromLocalProfile (_ profile: LocalProfile) -> Profile {
} }
public struct UserProfileUpdateSummary: Decodable { public struct UserProfileUpdateSummary: Decodable {
public var notChanged: Int
public var updateSuccesses: Int public var updateSuccesses: Int
public var updateFailures: Int public var updateFailures: Int
public var changedContacts: [Contact] public var changedContacts: [Contact]
+20 -4
View File
@@ -69,13 +69,29 @@ func fileModificationDate(_ path: String) -> Date? {
} }
} }
public func deleteAppDatabaseAndFiles() {
let fm = FileManager.default
let dbPath = getAppDatabasePath().path
do {
try fm.removeItem(atPath: dbPath + CHAT_DB)
try fm.removeItem(atPath: dbPath + AGENT_DB)
} catch let error {
logger.error("Failed to delete all databases: \(error)")
}
try? fm.removeItem(atPath: dbPath + CHAT_DB_BAK)
try? fm.removeItem(atPath: dbPath + AGENT_DB_BAK)
try? fm.removeItem(at: getTempFilesDirectory())
try? fm.createDirectory(at: getTempFilesDirectory(), withIntermediateDirectories: true)
deleteAppFiles()
_ = kcDatabasePassword.remove()
storeDBPassphraseGroupDefault.set(true)
}
public func deleteAppFiles() { public func deleteAppFiles() {
let fm = FileManager.default let fm = FileManager.default
do { do {
let fileNames = try fm.contentsOfDirectory(atPath: getAppFilesDirectory().path) try fm.removeItem(at: getAppFilesDirectory())
for fileName in fileNames { try fm.createDirectory(at: getAppFilesDirectory(), withIntermediateDirectories: true)
removeFile(fileName)
}
} catch { } catch {
logger.error("FileUtils deleteAppFiles error: \(error.localizedDescription)") logger.error("FileUtils deleteAppFiles error: \(error.localizedDescription)")
} }
+16
View File
@@ -23,3 +23,19 @@ void haskell_init(void) {
char **pargv = argv; char **pargv = argv;
hs_init_with_rtsopts(&argc, &pargv); hs_init_with_rtsopts(&argc, &pargv);
} }
void haskell_init_nse(void) {
int argc = 7;
char *argv[] = {
"simplex",
"+RTS", // requires `hs_init_with_rtsopts`
"-A1m", // chunk size for new allocations
"-H1m", // initial heap size
"-F0.5", // heap growth triggering GC
"-Fd1", // memory return
"-c", // compacting garbage collector
0
};
char **pargv = argv;
hs_init_with_rtsopts(&argc, &pargv);
}
+2
View File
@@ -11,4 +11,6 @@
void haskell_init(void); void haskell_init(void);
void haskell_init_nse(void);
#endif /* hs_init_h */ #endif /* hs_init_h */
@@ -3,11 +3,12 @@ package chat.simplex.app
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import androidx.work.* import androidx.work.*
import chat.simplex.app.*
import chat.simplex.app.SimplexService.Companion.showPassphraseNotification import chat.simplex.app.SimplexService.Companion.showPassphraseNotification
import chat.simplex.common.model.ChatController import chat.simplex.common.model.ChatController
import chat.simplex.common.views.helpers.DBMigrationResult import chat.simplex.common.views.helpers.DBMigrationResult
import chat.simplex.app.BuildConfig import chat.simplex.common.platform.chatModel
import chat.simplex.common.platform.initChatControllerAndRunMigrations
import chat.simplex.common.views.helpers.DatabaseUtils
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.util.Date import java.util.Date
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -57,6 +58,10 @@ class MessagesFetcherWork(
val durationSeconds = inputData.getInt(INPUT_DATA_DURATION, 60) val durationSeconds = inputData.getInt(INPUT_DATA_DURATION, 60)
var shouldReschedule = true var shouldReschedule = true
try { try {
// In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here
if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) {
initChatControllerAndRunMigrations()
}
withTimeout(durationSeconds * 1000L) { withTimeout(durationSeconds * 1000L) {
val chatController = ChatController val chatController = ChatController
SimplexService.waitDbMigrationEnds(chatController) SimplexService.waitDbMigrationEnds(chatController)
@@ -26,6 +26,7 @@ import kotlinx.coroutines.sync.withLock
import java.io.* import java.io.*
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.system.exitProcess
const val TAG = "SIMPLEX" const val TAG = "SIMPLEX"
@@ -46,8 +47,8 @@ class SimplexApp: Application(), LifecycleEventObserver {
try { try {
Looper.loop() Looper.loop()
} catch (e: Throwable) { } catch (e: Throwable) {
if (e.message != null && e.message!!.startsWith("Unable to start activity")) { if (e is UnsatisfiedLinkError || e.message?.startsWith("Unable to start activity") == true) {
android.os.Process.killProcess(android.os.Process.myPid()) Process.killProcess(Process.myPid())
break break
} else { } else {
// Send it to our exception handled because it will not get the exception otherwise // Send it to our exception handled because it will not get the exception otherwise
@@ -63,7 +64,9 @@ class SimplexApp: Application(), LifecycleEventObserver {
tmpDir.deleteRecursively() tmpDir.deleteRecursively()
tmpDir.mkdir() tmpDir.mkdir()
initChatControllerAndRunMigrations(false) if (DatabaseUtils.ksSelfDestructPassword.get() == null) {
initChatControllerAndRunMigrations()
}
ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp) ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp)
} }
@@ -77,7 +80,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
updatingChatsMutex.withLock { updatingChatsMutex.withLock {
kotlin.runCatching { kotlin.runCatching {
val currentUserId = chatModel.currentUser.value?.userId val currentUserId = chatModel.currentUser.value?.userId
val chats = ArrayList(chatController.apiGetChats(chatModel.remoteHostId())) val chats = ArrayList(chatController.apiGetChatsWithoutAlert(chatModel.remoteHostId()) ?: return@runCatching)
/** Active user can be changed in background while [ChatController.apiGetChats] is executing */ /** Active user can be changed in background while [ChatController.apiGetChats] is executing */
if (chatModel.currentUser.value?.userId == currentUserId) { if (chatModel.currentUser.value?.userId == currentUserId) {
val currentChatId = chatModel.chatId.value val currentChatId = chatModel.chatId.value
@@ -171,13 +174,14 @@ class SimplexApp: Application(), LifecycleEventObserver {
androidAppContext = this androidAppContext = this
APPLICATION_ID = BuildConfig.APPLICATION_ID APPLICATION_ID = BuildConfig.APPLICATION_ID
ntfManager = object : chat.simplex.common.platform.NtfManager() { ntfManager = object : chat.simplex.common.platform.NtfManager() {
override fun notifyCallInvitation(invitation: RcvCallInvitation) = NtfManager.notifyCallInvitation(invitation) override fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean = NtfManager.notifyCallInvitation(invitation)
override fun hasNotificationsForChat(chatId: String): Boolean = NtfManager.hasNotificationsForChat(chatId) override fun hasNotificationsForChat(chatId: String): Boolean = NtfManager.hasNotificationsForChat(chatId)
override fun cancelNotificationsForChat(chatId: String) = NtfManager.cancelNotificationsForChat(chatId) override fun cancelNotificationsForChat(chatId: String) = NtfManager.cancelNotificationsForChat(chatId)
override fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String?, actions: List<Pair<NotificationAction, () -> Unit>>) = NtfManager.displayNotification(user, chatId, displayName, msgText, image, actions.map { it.first }) override fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String?, actions: List<Pair<NotificationAction, () -> Unit>>) = NtfManager.displayNotification(user, chatId, displayName, msgText, image, actions.map { it.first })
override fun androidCreateNtfChannelsMaybeShowAlert() = NtfManager.createNtfChannelsMaybeShowAlert() override fun androidCreateNtfChannelsMaybeShowAlert() = NtfManager.createNtfChannelsMaybeShowAlert()
override fun cancelCallNotification() = NtfManager.cancelCallNotification() override fun cancelCallNotification() = NtfManager.cancelCallNotification()
override fun cancelAllNotifications() = NtfManager.cancelAllNotifications() override fun cancelAllNotifications() = NtfManager.cancelAllNotifications()
override fun showMessage(title: String, text: String) = NtfManager.showMessage(title, text)
} }
platform = object : PlatformInterface { platform = object : PlatformInterface {
override suspend fun androidServiceStart() { override suspend fun androidServiceStart() {
@@ -72,6 +72,10 @@ class SimplexService: Service() {
stopSelf() stopSelf()
} else { } else {
isServiceStarted = true isServiceStarted = true
// In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here
if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) {
initChatControllerAndRunMigrations()
}
} }
} }
@@ -30,7 +30,7 @@ object NtfManager {
const val ShowChatsAction: String = "chat.simplex.app.SHOW_CHATS" const val ShowChatsAction: String = "chat.simplex.app.SHOW_CHATS"
// DO NOT change notification channel settings / names // DO NOT change notification channel settings / names
const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION_1" const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION_2"
const val AcceptCallAction: String = "chat.simplex.app.ACCEPT_CALL" const val AcceptCallAction: String = "chat.simplex.app.ACCEPT_CALL"
const val RejectCallAction: String = "chat.simplex.app.REJECT_CALL" const val RejectCallAction: String = "chat.simplex.app.REJECT_CALL"
const val CallNotificationId: Int = -1 const val CallNotificationId: Int = -1
@@ -59,7 +59,7 @@ object NtfManager {
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.build() .build()
val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + R.raw.ring_once) val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/raw/ring_once")
Log.d(TAG, "callNotificationChannel sound: $soundUri") Log.d(TAG, "callNotificationChannel sound: $soundUri")
callChannel.setSound(soundUri, attrs) callChannel.setSound(soundUri, attrs)
callChannel.enableVibration(true) callChannel.enableVibration(true)
@@ -140,7 +140,7 @@ object NtfManager {
} }
} }
fun notifyCallInvitation(invitation: RcvCallInvitation) { fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean {
val keyguardManager = getKeyguardManager(context) val keyguardManager = getKeyguardManager(context)
Log.d( Log.d(
TAG, TAG,
@@ -149,7 +149,7 @@ object NtfManager {
"callOnLockScreen ${appPreferences.callOnLockScreen.get()}, " + "callOnLockScreen ${appPreferences.callOnLockScreen.get()}, " +
"onForeground ${isAppOnForeground}" "onForeground ${isAppOnForeground}"
) )
if (isAppOnForeground) return if (isAppOnForeground) return false
val contactId = invitation.contact.id val contactId = invitation.contact.id
Log.d(TAG, "notifyCallInvitation $contactId") Log.d(TAG, "notifyCallInvitation $contactId")
val image = invitation.contact.image val image = invitation.contact.image
@@ -163,7 +163,7 @@ object NtfManager {
.setFullScreenIntent(fullScreenPendingIntent, true) .setFullScreenIntent(fullScreenPendingIntent, true)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
} else { } else {
val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + R.raw.ring_once) val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/raw/ring_once")
val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
NotificationCompat.Builder(context, CallChannel) NotificationCompat.Builder(context, CallChannel)
.setContentIntent(chatPendingIntent(OpenChatAction, invitation.user.userId, invitation.contact.id)) .setContentIntent(chatPendingIntent(OpenChatAction, invitation.user.userId, invitation.contact.id))
@@ -206,6 +206,39 @@ object NtfManager {
notify(CallNotificationId, notification) notify(CallNotificationId, notification)
} }
} }
return true
}
fun showMessage(title: String, text: String) {
val builder = NotificationCompat.Builder(context, MessageChannel)
.setContentTitle(title)
.setContentText(text)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setGroup(MessageGroup)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
.setSmallIcon(R.drawable.ntf_icon)
.setLargeIcon(null)
.setColor(0x88FFFF)
.setAutoCancel(true)
.setVibrate(null)
.setContentIntent(chatPendingIntent(ShowChatsAction, null, null))
.setSilent(false)
val summary = NotificationCompat.Builder(context, MessageChannel)
.setSmallIcon(R.drawable.ntf_icon)
.setColor(0x88FFFF)
.setGroup(MessageGroup)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
.setGroupSummary(true)
.setContentIntent(chatPendingIntent(ShowChatsAction, null))
.build()
with(NotificationManagerCompat.from(context)) {
if (ActivityCompat.checkSelfPermission(SimplexApp.context, android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
notify("MESSAGE".hashCode(), builder.build())
notify(0, summary)
}
}
} }
fun cancelCallNotification() { fun cancelCallNotification() {
@@ -248,6 +281,7 @@ object NtfManager {
manager.createNotificationChannel(callNotificationChannel(CallChannel, generalGetString(MR.strings.ntf_channel_calls))) manager.createNotificationChannel(callNotificationChannel(CallChannel, generalGetString(MR.strings.ntf_channel_calls)))
// Remove old channels since they can't be edited // Remove old channels since they can't be edited
manager.deleteNotificationChannel("chat.simplex.app.CALL_NOTIFICATION") manager.deleteNotificationChannel("chat.simplex.app.CALL_NOTIFICATION")
manager.deleteNotificationChannel("chat.simplex.app.CALL_NOTIFICATION_1")
manager.deleteNotificationChannel("chat.simplex.app.LOCK_SCREEN_CALL_NOTIFICATION") manager.deleteNotificationChannel("chat.simplex.app.LOCK_SCREEN_CALL_NOTIFICATION")
} }
@@ -97,7 +97,8 @@ fun IncomingCallActivityView(m: ChatModel) {
Surface( Surface(
Modifier Modifier
.fillMaxSize(), .fillMaxSize(),
color = MaterialTheme.colors.background color = MaterialTheme.colors.background,
contentColor = LocalContentColor.current
) { ) {
if (showCallView) { if (showCallView) {
Box { Box {
@@ -200,7 +201,8 @@ private fun SimpleXLogo() {
private fun LockScreenCallButton(text: String, icon: Painter, color: Color, action: () -> Unit) { private fun LockScreenCallButton(text: String, icon: Painter, color: Color, action: () -> Unit) {
Surface( Surface(
shape = RoundedCornerShape(10.dp), shape = RoundedCornerShape(10.dp),
color = Color.Transparent color = Color.Transparent,
contentColor = LocalContentColor.current
) { ) {
Column( Column(
Modifier Modifier
@@ -227,7 +229,8 @@ fun PreviewIncomingCallLockScreenAlert() {
Surface( Surface(
Modifier Modifier
.fillMaxSize(), .fillMaxSize(),
color = MaterialTheme.colors.background color = MaterialTheme.colors.background,
contentColor = LocalContentColor.current
) { ) {
IncomingCallLockScreenAlertLayout( IncomingCallLockScreenAlertLayout(
invitation = RcvCallInvitation( invitation = RcvCallInvitation(
@@ -0,0 +1,7 @@
package chat.simplex.common.views.database
import chat.simplex.common.views.usersettings.restartApp
actual fun restartChatOrApp() {
restartApp()
}
@@ -28,7 +28,7 @@ actual fun SettingsSectionApp(
} }
private fun restartApp() { fun restartApp() {
ProcessPhoenix.triggerRebirth(androidAppContext) ProcessPhoenix.triggerRebirth(androidAppContext)
shutdownApp() shutdownApp()
} }
@@ -44,7 +44,7 @@ data class SettingsViewState(
fun AppScreen() { fun AppScreen() {
SimpleXTheme { SimpleXTheme {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
Surface(color = MaterialTheme.colors.background) { Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
MainScreen() MainScreen()
} }
} }
@@ -85,7 +85,7 @@ fun MainScreen() {
@Composable @Composable
fun AuthView() { fun AuthView() {
Surface(color = MaterialTheme.colors.background) { Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
Box( Box(
Modifier.fillMaxSize(), Modifier.fillMaxSize(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@@ -1,8 +1,7 @@
package chat.simplex.common package chat.simplex.common
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme import androidx.compose.material.*
import androidx.compose.material.Surface
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import chat.simplex.common.model.* import chat.simplex.common.model.*
@@ -107,7 +106,7 @@ object AppLock {
private fun setPasscode() { private fun setPasscode() {
val appPrefs = ChatController.appPrefs val appPrefs = ChatController.appPrefs
ModalManager.fullscreen.showCustomModal { close -> ModalManager.fullscreen.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
SetAppPasscodeView( SetAppPasscodeView(
submit = { submit = {
ChatModel.performLA.value = true ChatModel.performLA.value = true
@@ -125,6 +125,9 @@ object ChatModel {
val remoteHostPairing = mutableStateOf<Pair<RemoteHostInfo?, RemoteHostSessionState>?>(null) val remoteHostPairing = mutableStateOf<Pair<RemoteHostInfo?, RemoteHostSessionState>?>(null)
val remoteCtrlSession = mutableStateOf<RemoteCtrlSession?>(null) val remoteCtrlSession = mutableStateOf<RemoteCtrlSession?>(null)
val processedCriticalError: ProcessedErrors<AgentErrorType.CRITICAL> = ProcessedErrors(60_000)
val processedInternalError: ProcessedErrors<AgentErrorType.INTERNAL> = ProcessedErrors(20_000)
fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) { fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) {
currentUser.value currentUser.value
} else { } else {
@@ -1151,7 +1154,6 @@ data class LocalProfile(
@Serializable @Serializable
data class UserProfileUpdateSummary( data class UserProfileUpdateSummary(
val notChanged: Int,
val updateSuccesses: Int, val updateSuccesses: Int,
val updateFailures: Int, val updateFailures: Int,
val changedContacts: List<Contact> val changedContacts: List<Contact>
@@ -108,6 +108,7 @@ class AppPreferences {
val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null) val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null)
val chatStopped = mkBoolPreference(SHARED_PREFS_CHAT_STOPPED, false) val chatStopped = mkBoolPreference(SHARED_PREFS_CHAT_STOPPED, false)
val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false) val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false)
val showInternalErrors = mkBoolPreference(SHARED_PREFS_SHOW_INTERNAL_ERRORS, false)
val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, false) val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, false)
val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false) val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false)
val networkProxyHostPort = mkStrPreference(SHARED_PREFS_NETWORK_PROXY_HOST_PORT, "localhost:9050") val networkProxyHostPort = mkStrPreference(SHARED_PREFS_NETWORK_PROXY_HOST_PORT, "localhost:9050")
@@ -276,6 +277,7 @@ class AppPreferences {
private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart" private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart"
private const val SHARED_PREFS_CHAT_STOPPED = "ChatStopped" private const val SHARED_PREFS_CHAT_STOPPED = "ChatStopped"
private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools" private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools"
private const val SHARED_PREFS_SHOW_INTERNAL_ERRORS = "ShowInternalErrors"
private const val SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE = "TerminalAlwaysVisible" private const val SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE = "TerminalAlwaysVisible"
private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy" private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy"
private const val SHARED_PREFS_NETWORK_PROXY_HOST_PORT = "NetworkProxyHostPort" private const val SHARED_PREFS_NETWORK_PROXY_HOST_PORT = "NetworkProxyHostPort"
@@ -504,6 +506,10 @@ object ChatController {
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat && r.chatError.errorType is ChatErrorType.UserExists r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat && r.chatError.errorType is ChatErrorType.UserExists
) { ) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_create_user_duplicate_title), generalGetString(MR.strings.failed_to_create_user_duplicate_desc)) AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_create_user_duplicate_title), generalGetString(MR.strings.failed_to_create_user_duplicate_desc))
} else if (
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat && r.chatError.errorType is ChatErrorType.InvalidDisplayName
) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_create_user_invalid_title), generalGetString(MR.strings.failed_to_create_user_invalid_desc))
} else { } else {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_create_user_title), r.details) AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_create_user_title), r.details)
} }
@@ -572,7 +578,7 @@ object ChatController {
} }
suspend fun apiStartChat(): Boolean { suspend fun apiStartChat(): Boolean {
val r = sendCmd(null, CC.StartChat(expire = true)) val r = sendCmd(null, CC.StartChat(mainApp = true))
when (r) { when (r) {
is CR.ChatStarted -> return true is CR.ChatStarted -> return true
is CR.ChatRunning -> return false is CR.ChatRunning -> return false
@@ -648,6 +654,15 @@ object ChatController {
return emptyList() return emptyList()
} }
// It's useful for situations when active user can be changed concurrently and there is no need to show alert in case of failure
suspend fun apiGetChatsWithoutAlert(rh: Long?): List<Chat>? {
val userId = kotlin.runCatching { currentUserId("apiGetChats") }.getOrElse { return null }
val r = sendCmd(rh, CC.ApiGetChats(userId))
if (r is CR.ApiChats) return if (rh == null) r.chats else r.chats.map { it.copy(remoteHostId = rh) }
Log.e(TAG, "failed getting the list of chats: ${r.responseType} ${r.details}")
return null
}
suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, pagination: ChatPagination = ChatPagination.Last(ChatPagination.INITIAL_COUNT), search: String = ""): Chat? { suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, pagination: ChatPagination = ChatPagination.Last(ChatPagination.INITIAL_COUNT), search: String = ""): Chat? {
val r = sendCmd(rh, CC.ApiGetChat(type, id, pagination, search)) val r = sendCmd(rh, CC.ApiGetChat(type, id, pagination, search))
if (r is CR.ApiChat) return if (rh == null) r.chat else r.chat.copy(remoteHostId = rh) if (r is CR.ApiChat) return if (rh == null) r.chat else r.chat.copy(remoteHostId = rh)
@@ -1122,6 +1137,13 @@ object ChatController {
return false return false
} }
suspend fun apiGetCallInvitations(rh: Long?): List<RcvCallInvitation> {
val r = sendCmd(rh, CC.ApiGetCallInvitations())
if (r is CR.CallInvitations) return r.callInvitations
Log.e(TAG, "apiGetCallInvitations bad response: ${r.responseType} ${r.details}")
return emptyList()
}
suspend fun apiSendCallInvitation(rh: Long?, contact: Contact, callType: CallType): Boolean { suspend fun apiSendCallInvitation(rh: Long?, contact: Contact, callType: CallType): Boolean {
val r = sendCmd(rh, CC.ApiSendCallInvitation(contact, callType)) val r = sendCmd(rh, CC.ApiSendCallInvitation(contact, callType))
return r is CR.CmdOk return r is CR.CmdOk
@@ -1878,9 +1900,34 @@ object ChatController {
val disconnectedHost = chatModel.remoteHosts.firstOrNull { it.remoteHostId == r.remoteHostId_ } val disconnectedHost = chatModel.remoteHosts.firstOrNull { it.remoteHostId == r.remoteHostId_ }
chatModel.remoteHostPairing.value = null chatModel.remoteHostPairing.value = null
if (disconnectedHost != null) { if (disconnectedHost != null) {
showToast( val deviceName = disconnectedHost.hostDeviceName.ifEmpty { disconnectedHost.remoteHostId.toString() }
generalGetString(MR.strings.remote_host_was_disconnected_toast).format(disconnectedHost.hostDeviceName.ifEmpty { disconnectedHost.remoteHostId.toString() }) when (r.rhStopReason) {
) is RemoteHostStopReason.ConnectionFailed -> {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.remote_host_was_disconnected_title),
if (r.rhStopReason.chatError is ChatError.ChatErrorRemoteHost) {
r.rhStopReason.chatError.remoteHostError.localizedString(deviceName)
} else {
generalGetString(MR.strings.remote_host_disconnected_from).format(deviceName, r.rhStopReason.chatError.string)
}
)
}
is RemoteHostStopReason.Crashed -> {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.remote_host_was_disconnected_title),
if (r.rhStopReason.chatError is ChatError.ChatErrorRemoteHost) {
r.rhStopReason.chatError.remoteHostError.localizedString(deviceName)
} else {
generalGetString(MR.strings.remote_host_disconnected_from).format(deviceName, r.rhStopReason.chatError.string)
}
)
}
is RemoteHostStopReason.Disconnected -> {
if (r.rhsState is RemoteHostSessionState.Connected || r.rhsState is RemoteHostSessionState.Confirmed) {
showToast(generalGetString(MR.strings.remote_host_was_disconnected_toast).format(deviceName))
}
}
}
} }
if (chatModel.remoteHostId() == r.remoteHostId_) { if (chatModel.remoteHostId() == r.remoteHostId_) {
chatModel.currentRemoteHost.value = null chatModel.currentRemoteHost.value = null
@@ -1911,11 +1958,40 @@ object ChatController {
val sess = chatModel.remoteCtrlSession.value val sess = chatModel.remoteCtrlSession.value
if (sess != null) { if (sess != null) {
chatModel.remoteCtrlSession.value = null chatModel.remoteCtrlSession.value = null
fun showAlert(chatError: ChatError) {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.remote_ctrl_was_disconnected_title),
if (chatError is ChatError.ChatErrorRemoteCtrl) {
chatError.remoteCtrlError.localizedString
} else {
generalGetString(MR.strings.remote_ctrl_disconnected_with_reason).format(chatError.string)
}
)
}
when (r.rcStopReason) {
is RemoteCtrlStopReason.DiscoveryFailed -> showAlert(r.rcStopReason.chatError)
is RemoteCtrlStopReason.ConnectionFailed -> showAlert(r.rcStopReason.chatError)
is RemoteCtrlStopReason.SetupFailed -> showAlert(r.rcStopReason.chatError)
is RemoteCtrlStopReason.Disconnected -> {
/*AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.remote_ctrl_was_disconnected_title),
)*/
}
}
if (sess.sessionState is UIRemoteCtrlSessionState.Connected) { if (sess.sessionState is UIRemoteCtrlSessionState.Connected) {
switchToLocalSession() switchToLocalSession()
} }
} }
} }
is CR.ChatCmdError -> when {
r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.CRITICAL -> {
chatModel.processedCriticalError.newError(r.chatError.agentError, r.chatError.agentError.offerRestart)
}
r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.INTERNAL && appPrefs.showInternalErrors.get() -> {
chatModel.processedInternalError.newError(r.chatError.agentError, false)
}
}
else -> else ->
Log.d(TAG , "unsupported event: ${r.responseType}") Log.d(TAG , "unsupported event: ${r.responseType}")
} }
@@ -2165,7 +2241,7 @@ sealed class CC {
class ApiMuteUser(val userId: Long): CC() class ApiMuteUser(val userId: Long): CC()
class ApiUnmuteUser(val userId: Long): CC() class ApiUnmuteUser(val userId: Long): CC()
class ApiDeleteUser(val userId: Long, val delSMPQueues: Boolean, val viewPwd: String?): CC() class ApiDeleteUser(val userId: Long, val delSMPQueues: Boolean, val viewPwd: String?): CC()
class StartChat(val expire: Boolean): CC() class StartChat(val mainApp: Boolean): CC()
class ApiStopChat: CC() class ApiStopChat: CC()
class SetTempFolder(val tempFolder: String): CC() class SetTempFolder(val tempFolder: String): CC()
class SetFilesFolder(val filesFolder: String): CC() class SetFilesFolder(val filesFolder: String): CC()
@@ -2236,6 +2312,7 @@ sealed class CC {
class ApiShowMyAddress(val userId: Long): CC() class ApiShowMyAddress(val userId: Long): CC()
class ApiSetProfileAddress(val userId: Long, val on: Boolean): CC() class ApiSetProfileAddress(val userId: Long, val on: Boolean): CC()
class ApiAddressAutoAccept(val userId: Long, val autoAccept: AutoAccept?): CC() class ApiAddressAutoAccept(val userId: Long, val autoAccept: AutoAccept?): CC()
class ApiGetCallInvitations: CC()
class ApiSendCallInvitation(val contact: Contact, val callType: CallType): CC() class ApiSendCallInvitation(val contact: Contact, val callType: CallType): CC()
class ApiRejectCall(val contact: Contact): CC() class ApiRejectCall(val contact: Contact): CC()
class ApiSendCallOffer(val contact: Contact, val callOffer: WebRTCCallOffer): CC() class ApiSendCallOffer(val contact: Contact, val callOffer: WebRTCCallOffer): CC()
@@ -2292,7 +2369,7 @@ sealed class CC {
is ApiMuteUser -> "/_mute user $userId" is ApiMuteUser -> "/_mute user $userId"
is ApiUnmuteUser -> "/_unmute user $userId" is ApiUnmuteUser -> "/_unmute user $userId"
is ApiDeleteUser -> "/_delete user $userId del_smp=${onOff(delSMPQueues)}${maybePwd(viewPwd)}" is ApiDeleteUser -> "/_delete user $userId del_smp=${onOff(delSMPQueues)}${maybePwd(viewPwd)}"
is StartChat -> "/_start subscribe=on expire=${onOff(expire)} xftp=on" is StartChat -> "/_start main=${onOff(mainApp)}"
is ApiStopChat -> "/_stop" is ApiStopChat -> "/_stop"
is SetTempFolder -> "/_temp_folder $tempFolder" is SetTempFolder -> "/_temp_folder $tempFolder"
is SetFilesFolder -> "/_files_folder $filesFolder" is SetFilesFolder -> "/_files_folder $filesFolder"
@@ -2372,6 +2449,7 @@ sealed class CC {
is ApiAddressAutoAccept -> "/_auto_accept $userId ${AutoAccept.cmdString(autoAccept)}" is ApiAddressAutoAccept -> "/_auto_accept $userId ${AutoAccept.cmdString(autoAccept)}"
is ApiAcceptContact -> "/_accept incognito=${onOff(incognito)} $contactReqId" is ApiAcceptContact -> "/_accept incognito=${onOff(incognito)} $contactReqId"
is ApiRejectContact -> "/_reject $contactReqId" is ApiRejectContact -> "/_reject $contactReqId"
is ApiGetCallInvitations -> "/_call get"
is ApiSendCallInvitation -> "/_call invite @${contact.apiId} ${json.encodeToString(callType)}" is ApiSendCallInvitation -> "/_call invite @${contact.apiId} ${json.encodeToString(callType)}"
is ApiRejectCall -> "/_call reject @${contact.apiId}" is ApiRejectCall -> "/_call reject @${contact.apiId}"
is ApiSendCallOffer -> "/_call offer @${contact.apiId} ${json.encodeToString(callOffer)}" is ApiSendCallOffer -> "/_call offer @${contact.apiId} ${json.encodeToString(callOffer)}"
@@ -2495,6 +2573,7 @@ sealed class CC {
is ApiAddressAutoAccept -> "apiAddressAutoAccept" is ApiAddressAutoAccept -> "apiAddressAutoAccept"
is ApiAcceptContact -> "apiAcceptContact" is ApiAcceptContact -> "apiAcceptContact"
is ApiRejectContact -> "apiRejectContact" is ApiRejectContact -> "apiRejectContact"
is ApiGetCallInvitations -> "apiGetCallInvitations"
is ApiSendCallInvitation -> "apiSendCallInvitation" is ApiSendCallInvitation -> "apiSendCallInvitation"
is ApiRejectCall -> "apiRejectCall" is ApiRejectCall -> "apiRejectCall"
is ApiSendCallOffer -> "apiSendCallOffer" is ApiSendCallOffer -> "apiSendCallOffer"
@@ -3870,6 +3949,7 @@ sealed class CR {
@Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem: AChatItem): CR() @Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem: AChatItem): CR()
// call events // call events
@Serializable @SerialName("callInvitation") class CallInvitation(val callInvitation: RcvCallInvitation): CR() @Serializable @SerialName("callInvitation") class CallInvitation(val callInvitation: RcvCallInvitation): CR()
@Serializable @SerialName("callInvitations") class CallInvitations(val callInvitations: List<RcvCallInvitation>): CR()
@Serializable @SerialName("callOffer") class CallOffer(val user: UserRef, val contact: Contact, val callType: CallType, val offer: WebRTCSession, val sharedKey: String? = null, val askConfirmation: Boolean): CR() @Serializable @SerialName("callOffer") class CallOffer(val user: UserRef, val contact: Contact, val callType: CallType, val offer: WebRTCSession, val sharedKey: String? = null, val askConfirmation: Boolean): CR()
@Serializable @SerialName("callAnswer") class CallAnswer(val user: UserRef, val contact: Contact, val answer: WebRTCSession): CR() @Serializable @SerialName("callAnswer") class CallAnswer(val user: UserRef, val contact: Contact, val answer: WebRTCSession): CR()
@Serializable @SerialName("callExtraInfo") class CallExtraInfo(val user: UserRef, val contact: Contact, val extraInfo: WebRTCExtraInfo): CR() @Serializable @SerialName("callExtraInfo") class CallExtraInfo(val user: UserRef, val contact: Contact, val extraInfo: WebRTCExtraInfo): CR()
@@ -4017,6 +4097,7 @@ sealed class CR {
is SndFileProgressXFTP -> "sndFileProgressXFTP" is SndFileProgressXFTP -> "sndFileProgressXFTP"
is SndFileCompleteXFTP -> "sndFileCompleteXFTP" is SndFileCompleteXFTP -> "sndFileCompleteXFTP"
is SndFileError -> "sndFileError" is SndFileError -> "sndFileError"
is CallInvitations -> "callInvitations"
is CallInvitation -> "callInvitation" is CallInvitation -> "callInvitation"
is CallOffer -> "callOffer" is CallOffer -> "callOffer"
is CallAnswer -> "callAnswer" is CallAnswer -> "callAnswer"
@@ -4163,6 +4244,7 @@ sealed class CR {
is SndFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\nsentSize: $sentSize\ntotalSize: $totalSize") is SndFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\nsentSize: $sentSize\ntotalSize: $totalSize")
is SndFileCompleteXFTP -> withUser(user, json.encodeToString(chatItem)) is SndFileCompleteXFTP -> withUser(user, json.encodeToString(chatItem))
is SndFileError -> withUser(user, json.encodeToString(chatItem)) is SndFileError -> withUser(user, json.encodeToString(chatItem))
is CallInvitations -> "callInvitations: ${json.encodeToString(callInvitations)}"
is CallInvitation -> "contact: ${callInvitation.contact.id}\ncallType: $callInvitation.callType\nsharedKey: ${callInvitation.sharedKey ?: ""}" is CallInvitation -> "contact: ${callInvitation.contact.id}\ncallType: $callInvitation.callType\nsharedKey: ${callInvitation.sharedKey ?: ""}"
is CallOffer -> withUser(user, "contact: ${contact.id}\ncallType: $callType\nsharedKey: ${sharedKey ?: ""}\naskConfirmation: $askConfirmation\noffer: ${json.encodeToString(offer)}") is CallOffer -> withUser(user, "contact: ${contact.id}\ncallType: $callType\nsharedKey: ${sharedKey ?: ""}\naskConfirmation: $askConfirmation\noffer: ${json.encodeToString(offer)}")
is CallAnswer -> withUser(user, "contact: ${contact.id}\nanswer: ${json.encodeToString(answer)}") is CallAnswer -> withUser(user, "contact: ${contact.id}\nanswer: ${json.encodeToString(answer)}")
@@ -4437,6 +4519,7 @@ sealed class ChatErrorType {
is EmptyUserPassword -> "emptyUserPassword" is EmptyUserPassword -> "emptyUserPassword"
is UserAlreadyHidden -> "userAlreadyHidden" is UserAlreadyHidden -> "userAlreadyHidden"
is UserNotHidden -> "userNotHidden" is UserNotHidden -> "userNotHidden"
is InvalidDisplayName -> "invalidDisplayName"
is ChatNotStarted -> "chatNotStarted" is ChatNotStarted -> "chatNotStarted"
is ChatNotStopped -> "chatNotStopped" is ChatNotStopped -> "chatNotStopped"
is ChatStoreChanged -> "chatStoreChanged" is ChatStoreChanged -> "chatStoreChanged"
@@ -4514,6 +4597,7 @@ sealed class ChatErrorType {
@Serializable @SerialName("emptyUserPassword") class EmptyUserPassword(val userId: Long): ChatErrorType() @Serializable @SerialName("emptyUserPassword") class EmptyUserPassword(val userId: Long): ChatErrorType()
@Serializable @SerialName("userAlreadyHidden") class UserAlreadyHidden(val userId: Long): ChatErrorType() @Serializable @SerialName("userAlreadyHidden") class UserAlreadyHidden(val userId: Long): ChatErrorType()
@Serializable @SerialName("userNotHidden") class UserNotHidden(val userId: Long): ChatErrorType() @Serializable @SerialName("userNotHidden") class UserNotHidden(val userId: Long): ChatErrorType()
@Serializable @SerialName("invalidDisplayName") object InvalidDisplayName: ChatErrorType()
@Serializable @SerialName("chatNotStarted") object ChatNotStarted: ChatErrorType() @Serializable @SerialName("chatNotStarted") object ChatNotStarted: ChatErrorType()
@Serializable @SerialName("chatNotStopped") object ChatNotStopped: ChatErrorType() @Serializable @SerialName("chatNotStopped") object ChatNotStopped: ChatErrorType()
@Serializable @SerialName("chatStoreChanged") object ChatStoreChanged: ChatErrorType() @Serializable @SerialName("chatStoreChanged") object ChatStoreChanged: ChatErrorType()
@@ -4731,6 +4815,7 @@ sealed class AgentErrorType {
is AGENT -> "AGENT ${agentErr.string}" is AGENT -> "AGENT ${agentErr.string}"
is INTERNAL -> "INTERNAL $internalErr" is INTERNAL -> "INTERNAL $internalErr"
is INACTIVE -> "INACTIVE" is INACTIVE -> "INACTIVE"
is CRITICAL -> "CRITICAL $offerRestart $criticalErr"
} }
@Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType): AgentErrorType() @Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType): AgentErrorType()
@Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType): AgentErrorType() @Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType): AgentErrorType()
@@ -4742,6 +4827,7 @@ sealed class AgentErrorType {
@Serializable @SerialName("AGENT") class AGENT(val agentErr: SMPAgentError): AgentErrorType() @Serializable @SerialName("AGENT") class AGENT(val agentErr: SMPAgentError): AgentErrorType()
@Serializable @SerialName("INTERNAL") class INTERNAL(val internalErr: String): AgentErrorType() @Serializable @SerialName("INTERNAL") class INTERNAL(val internalErr: String): AgentErrorType()
@Serializable @SerialName("INACTIVE") object INACTIVE: AgentErrorType() @Serializable @SerialName("INACTIVE") object INACTIVE: AgentErrorType()
@Serializable @SerialName("CRITICAL") data class CRITICAL(val offerRestart: Boolean, val criticalErr: String): AgentErrorType()
} }
@Serializable @Serializable
@@ -4961,6 +5047,15 @@ sealed class RemoteHostError {
is BadVersion -> "badVersion" is BadVersion -> "badVersion"
is Disconnected -> "disconnected" is Disconnected -> "disconnected"
} }
fun localizedString(name: String): String = when (this) {
is Missing -> generalGetString(MR.strings.remote_host_error_missing)
is Inactive -> generalGetString(MR.strings.remote_host_error_inactive)
is Busy -> generalGetString(MR.strings.remote_host_error_busy)
is Timeout -> generalGetString(MR.strings.remote_host_error_timeout)
is BadState -> generalGetString(MR.strings.remote_host_error_bad_state)
is BadVersion -> generalGetString(MR.strings.remote_host_error_bad_version)
is Disconnected -> generalGetString(MR.strings.remote_host_error_disconnected)
}.format(name)
@Serializable @SerialName("missing") object Missing: RemoteHostError() @Serializable @SerialName("missing") object Missing: RemoteHostError()
@Serializable @SerialName("inactive") object Inactive: RemoteHostError() @Serializable @SerialName("inactive") object Inactive: RemoteHostError()
@Serializable @SerialName("busy") object Busy: RemoteHostError() @Serializable @SerialName("busy") object Busy: RemoteHostError()
@@ -4981,6 +5076,16 @@ sealed class RemoteCtrlError {
is BadInvitation -> "badInvitation" is BadInvitation -> "badInvitation"
is BadVersion -> "badVersion" is BadVersion -> "badVersion"
} }
val localizedString: String get() = when (this) {
is Inactive -> generalGetString(MR.strings.remote_ctrl_error_inactive)
is BadState -> generalGetString(MR.strings.remote_ctrl_error_bad_state)
is Busy -> generalGetString(MR.strings.remote_ctrl_error_busy)
is Timeout -> generalGetString(MR.strings.remote_ctrl_error_timeout)
is Disconnected -> generalGetString(MR.strings.remote_ctrl_error_disconnected)
is BadInvitation -> generalGetString(MR.strings.remote_ctrl_error_bad_invitation)
is BadVersion -> generalGetString(MR.strings.remote_ctrl_error_bad_version)
}
@Serializable @SerialName("inactive") object Inactive: RemoteCtrlError() @Serializable @SerialName("inactive") object Inactive: RemoteCtrlError()
@Serializable @SerialName("badState") object BadState: RemoteCtrlError() @Serializable @SerialName("badState") object BadState: RemoteCtrlError()
@Serializable @SerialName("busy") object Busy: RemoteCtrlError() @Serializable @SerialName("busy") object Busy: RemoteCtrlError()
@@ -41,21 +41,20 @@ val appPreferences: AppPreferences
val chatController: ChatController = ChatController val chatController: ChatController = ChatController
fun initChatControllerAndRunMigrations(ignoreSelfDestruct: Boolean) { fun initChatControllerAndRunMigrations() {
if (ignoreSelfDestruct || DatabaseUtils.ksSelfDestructPassword.get() == null) { withBGApi {
withBGApi { if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) {
if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) { initChatController(startChat = ::showStartChatAfterRestartAlert)
initChatController(startChat = ::showStartChatAfterRestartAlert) } else {
} else { initChatController()
initChatController()
}
runMigrations()
} }
runMigrations()
} }
} }
suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: () -> CompletableDeferred<Boolean> = { CompletableDeferred(true) }) { suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: () -> CompletableDeferred<Boolean> = { CompletableDeferred(true) }) {
try { try {
if (chatModel.ctrlInitInProgress.value) return
chatModel.ctrlInitInProgress.value = true chatModel.ctrlInitInProgress.value = true
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
@@ -93,12 +93,13 @@ abstract class NtfManager {
} }
} }
abstract fun notifyCallInvitation(invitation: RcvCallInvitation) abstract fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean
abstract fun hasNotificationsForChat(chatId: String): Boolean abstract fun hasNotificationsForChat(chatId: String): Boolean
abstract fun cancelNotificationsForChat(chatId: String) abstract fun cancelNotificationsForChat(chatId: String)
abstract fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List<Pair<NotificationAction, () -> Unit>> = emptyList()) abstract fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List<Pair<NotificationAction, () -> Unit>> = emptyList())
abstract fun cancelCallNotification() abstract fun cancelCallNotification()
abstract fun cancelAllNotifications() abstract fun cancelAllNotifications()
abstract fun showMessage(title: String, text: String)
// Android only // Android only
abstract fun androidCreateNtfChannelsMaybeShowAlert() abstract fun androidCreateNtfChannelsMaybeShowAlert()
@@ -1,5 +1,7 @@
package chat.simplex.common.ui.theme package chat.simplex.common.ui.theme
import androidx.compose.material.LocalContentColor
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
val Purple200 = Color(0xFFBB86FC) val Purple200 = Color(0xFFBB86FC)
@@ -25,4 +27,5 @@ val WarningOrange = Color(255, 127, 0, 255)
val WarningYellow = Color(255, 192, 0, 255) val WarningYellow = Color(255, 192, 0, 255)
val FileLight = Color(183, 190, 199, 255) val FileLight = Color(183, 190, 199, 255)
val FileDark = Color(101, 101, 106, 255) val FileDark = Color(101, 101, 106, 255)
val MenuTextColorDark = Color.White.copy(alpha = 0.8f)
val MenuTextColor: Color @Composable get () = if (isInDarkTheme()) LocalContentColor.current.copy(alpha = 0.8f) else Color.Black
@@ -283,27 +283,10 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
val theme by CurrentColors.collectAsState() val theme by CurrentColors.collectAsState()
MaterialTheme( MaterialTheme(
colors = theme.colors, colors = theme.colors,
typography = Typography.copy( typography = Typography,
h1 = Typography.h1.copy(color = theme.colors.onBackground),
h2 = Typography.h2.copy(color = theme.colors.onBackground),
h3 = Typography.h3.copy(color = theme.colors.onBackground),
h4 = Typography.h4.copy(color = theme.colors.onBackground),
h5 = Typography.h5.copy(color = theme.colors.onBackground),
h6 = Typography.h6.copy(color = theme.colors.onBackground),
subtitle1 = Typography.subtitle1.copy(color = theme.colors.onBackground),
subtitle2 = Typography.subtitle2.copy(color = theme.colors.onBackground),
body1 = Typography.body1.copy(color = theme.colors.onBackground),
body2 = Typography.body2.copy(color = theme.colors.onBackground),
button = Typography.button.copy(color = theme.colors.onBackground),
caption = Typography.caption.copy(color = theme.colors.onBackground),
overline = Typography.overline.copy(color = theme.colors.onBackground)
),
shapes = Shapes, shapes = Shapes,
content = { content = {
ProvideTextStyle( CompositionLocalProvider(LocalContentColor provides MaterialTheme.colors.onBackground, content = content)
value = TextStyle(color = theme.colors.onBackground),
content = content
)
} }
) )
} }
@@ -1,8 +1,7 @@
package chat.simplex.common.views package chat.simplex.common.views
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme import androidx.compose.material.*
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -11,7 +10,8 @@ fun SplashView() {
Surface( Surface(
Modifier Modifier
.fillMaxSize(), .fillMaxSize(),
color = MaterialTheme.colors.background color = MaterialTheme.colors.background,
contentColor = LocalContentColor.current
) { ) {
// Image( // Image(
// painter = painterResource(MR.images.logo), // painter = painterResource(MR.images.logo),
@@ -101,13 +101,16 @@ fun TerminalLayout(
) )
} }
}, },
contentColor = LocalContentColor.current,
drawerContentColor = LocalContentColor.current,
modifier = Modifier.navigationBarsWithImePadding() modifier = Modifier.navigationBarsWithImePadding()
) { contentPadding -> ) { contentPadding ->
Surface( Surface(
modifier = Modifier modifier = Modifier
.padding(contentPadding) .padding(contentPadding)
.fillMaxWidth(), .fillMaxWidth(),
color = MaterialTheme.colors.background color = MaterialTheme.colors.background,
contentColor = LocalContentColor.current
) { ) {
TerminalLog() TerminalLog()
} }
@@ -239,7 +239,7 @@ fun OnboardingButtons(displayName: MutableState<String>, close: () -> Unit) {
val enabled = canCreateProfile(displayName.value) val enabled = canCreateProfile(displayName.value)
val createModifier: Modifier = Modifier.clickable(enabled) { createProfileOnboarding(chatModel, displayName.value, close) }.padding(8.dp) val createModifier: Modifier = Modifier.clickable(enabled) { createProfileOnboarding(chatModel, displayName.value, close) }.padding(8.dp)
val createColor: Color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary val createColor: Color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
Surface(shape = RoundedCornerShape(20.dp), color = Color.Transparent) { Surface(shape = RoundedCornerShape(20.dp), color = Color.Transparent, contentColor = LocalContentColor.current) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) { Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) {
Text(stringResource(MR.strings.create_profile_button), style = MaterialTheme.typography.caption, color = createColor, fontWeight = FontWeight.Medium) Text(stringResource(MR.strings.create_profile_button), style = MaterialTheme.typography.caption, color = createColor, fontWeight = FontWeight.Medium)
Icon(painterResource(MR.images.ic_arrow_forward_ios), stringResource(MR.strings.create_profile_button), tint = createColor) Icon(painterResource(MR.images.ic_arrow_forward_ios), stringResource(MR.strings.create_profile_button), tint = createColor)
@@ -13,8 +13,8 @@ class CallManager(val chatModel: ChatModel) {
callInvitations[invitation.contact.id] = invitation callInvitations[invitation.contact.id] = invitation
if (invitation.user.showNotifications) { if (invitation.user.showNotifications) {
if (Clock.System.now() - invitation.callTs <= 3.minutes) { if (Clock.System.now() - invitation.callTs <= 3.minutes) {
invitation.sentNotification = ntfManager.notifyCallInvitation(invitation)
activeCallInvitation.value = invitation activeCallInvitation.value = invitation
ntfManager.notifyCallInvitation(invitation)
} else { } else {
val contact = invitation.contact val contact = invitation.contact
ntfManager.displayNotification(user = invitation.user, chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText) ntfManager.displayNotification(user = invitation.user, chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText)
@@ -15,11 +15,10 @@ import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.ProfileImage import chat.simplex.common.views.helpers.ProfileImage
import chat.simplex.common.views.usersettings.ProfilePreview import chat.simplex.common.views.usersettings.ProfilePreview
import chat.simplex.common.platform.ntfManager
import chat.simplex.common.platform.SoundPlayer
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
@@ -27,7 +26,11 @@ import kotlinx.datetime.Clock
fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) { fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) {
val cm = chatModel.callManager val cm = chatModel.callManager
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
LaunchedEffect(true) { SoundPlayer.start(scope, sound = !chatModel.showCallView.value) } LaunchedEffect(Unit) {
if (chatModel.activeCallInvitation.value?.sentNotification == false || appPlatform.isDesktop) {
SoundPlayer.start(scope, sound = !chatModel.showCallView.value)
}
}
DisposableEffect(true) { onDispose { SoundPlayer.stop() } } DisposableEffect(true) { onDispose { SoundPlayer.stop() } }
IncomingCallAlertLayout( IncomingCallAlertLayout(
invitation, invitation,
@@ -85,7 +88,8 @@ fun IncomingCallInfo(invitation: RcvCallInvitation, chatModel: ChatModel) {
private fun CallButton(text: String, icon: Painter, color: Color, action: () -> Unit) { private fun CallButton(text: String, icon: Painter, color: Color, action: () -> Unit) {
Surface( Surface(
shape = RoundedCornerShape(10.dp), shape = RoundedCornerShape(10.dp),
color = Color.Transparent color = Color.Transparent,
contentColor = LocalContentColor.current
) { ) {
Column( Column(
Modifier Modifier
@@ -112,6 +112,9 @@ sealed class WCallResponse {
CallMediaType.Video -> MR.strings.incoming_video_call CallMediaType.Video -> MR.strings.incoming_video_call
CallMediaType.Audio -> MR.strings.incoming_audio_call CallMediaType.Audio -> MR.strings.incoming_audio_call
}) })
// Shows whether notification was shown or not to prevent playing sound twice in both notification and in-app
var sentNotification: Boolean = false
} }
@Serializable data class CallCapabilities(val encryption: Boolean) @Serializable data class CallCapabilities(val encryption: Boolean)
@Serializable data class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) { @Serializable data class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) {
@@ -24,6 +24,7 @@ import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.text.* import androidx.compose.ui.text.*
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.* import chat.simplex.common.views.call.*
import chat.simplex.common.views.chat.group.* import chat.simplex.common.views.chat.group.*
@@ -317,11 +318,14 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
}, },
acceptCall = { contact -> acceptCall = { contact ->
hideKeyboard(view) hideKeyboard(view)
val invitation = chatModel.callInvitations.remove(contact.id) withApi {
if (invitation == null) { val invitation = chatModel.callInvitations.remove(contact.id)
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended)) ?: controller.apiGetCallInvitations(chatModel.remoteHostId()).firstOrNull { it.contact.id == contact.id }
} else { if (invitation == null) {
chatModel.callManager.acceptIncomingCall(invitation = invitation) AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended))
} else {
chatModel.callManager.acceptIncomingCall(invitation = invitation)
}
} }
}, },
acceptFeature = { contact, feature, param -> acceptFeature = { contact, feature, param ->
@@ -570,6 +574,8 @@ fun ChatLayout(
bottomBar = composeView, bottomBar = composeView,
modifier = Modifier.navigationBarsWithImePadding(), modifier = Modifier.navigationBarsWithImePadding(),
floatingActionButton = { floatingButton.value() }, floatingActionButton = { floatingButton.value() },
contentColor = LocalContentColor.current,
drawerContentColor = LocalContentColor.current,
) { contentPadding -> ) { contentPadding ->
BoxWithConstraints(Modifier BoxWithConstraints(Modifier
.fillMaxHeight() .fillMaxHeight()
@@ -258,7 +258,8 @@ private fun CustomDisappearingMessageDialog(
DefaultDialog(onDismissRequest = { setShowDialog(false) }) { DefaultDialog(onDismissRequest = { setShowDialog(false) }) {
Surface( Surface(
shape = RoundedCornerShape(corner = CornerSize(25.dp)) shape = RoundedCornerShape(corner = CornerSize(25.dp)),
contentColor = LocalContentColor.current
) { ) {
Box( Box(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@@ -131,7 +131,8 @@ fun CIFileView(
Surface( Surface(
Modifier.drawRingModifier(angle, strokeColor, strokeWidth), Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
color = Color.Transparent, color = Color.Transparent,
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)) shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
contentColor = LocalContentColor.current
) { ) {
Box(Modifier.size(32.dp)) Box(Modifier.size(32.dp))
} }
@@ -88,6 +88,7 @@ fun CIGroupInvitationView(
}) else Modifier, }) else Modifier,
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
color = if (sent) sentColor else receivedColor, color = if (sent) sentColor else receivedColor,
contentColor = LocalContentColor.current
) { ) {
Box( Box(
Modifier Modifier
@@ -142,6 +142,7 @@ fun DecryptionErrorItemFixButton(
Modifier.clickable(onClick = onClick), Modifier.clickable(onClick = onClick),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
color = receivedColor, color = receivedColor,
contentColor = LocalContentColor.current
) { ) {
Box( Box(
Modifier.padding(vertical = 6.dp, horizontal = 12.dp), Modifier.padding(vertical = 6.dp, horizontal = 12.dp),
@@ -188,6 +189,7 @@ fun DecryptionErrorItem(
Modifier.clickable(onClick = onClick), Modifier.clickable(onClick = onClick),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
color = receivedColor, color = receivedColor,
contentColor = LocalContentColor.current
) { ) {
Box( Box(
Modifier.padding(vertical = 6.dp, horizontal = 12.dp), Modifier.padding(vertical = 6.dp, horizontal = 12.dp),
@@ -153,7 +153,8 @@ private fun BoxScope.PlayButton(error: Boolean = false, onLongClick: () -> Unit,
Surface( Surface(
Modifier.align(Alignment.Center), Modifier.align(Alignment.Center),
color = Color.Black.copy(alpha = 0.25f), color = Color.Black.copy(alpha = 0.25f),
shape = RoundedCornerShape(percent = 50) shape = RoundedCornerShape(percent = 50),
contentColor = LocalContentColor.current
) { ) {
Box( Box(
Modifier Modifier
@@ -264,7 +265,8 @@ private fun progressCircle(progress: Long, total: Long) {
Surface( Surface(
Modifier.drawRingModifier(angle, strokeColor, strokeWidth), Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
color = Color.Transparent, color = Color.Transparent,
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)) shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
contentColor = LocalContentColor.current
) { ) {
Box(Modifier.size(16.dp)) Box(Modifier.size(16.dp))
} }
@@ -225,7 +225,8 @@ private fun PlayPauseButton(
Surface( Surface(
Modifier.drawRingModifier(angle, strokeColor, strokeWidth), Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
color = if (sent) sentColor else receivedColor, color = if (sent) sentColor else receivedColor,
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)) shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
contentColor = LocalContentColor.current
) { ) {
Box( Box(
Modifier Modifier
@@ -613,7 +613,7 @@ private fun ShrinkItemAction(revealed: MutableState<Boolean>, showMenu: MutableS
@Composable @Composable
fun ItemAction(text: String, icon: Painter, color: Color = Color.Unspecified, onClick: () -> Unit) { fun ItemAction(text: String, icon: Painter, color: Color = Color.Unspecified, onClick: () -> Unit) {
val finalColor = if (color == Color.Unspecified) { val finalColor = if (color == Color.Unspecified) {
if (isInDarkTheme()) MenuTextColorDark else Color.Black MenuTextColor
} else color } else color
DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) { DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
@@ -633,7 +633,7 @@ fun ItemAction(text: String, icon: Painter, color: Color = Color.Unspecified, on
@Composable @Composable
fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Color = Color.Unspecified) { fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Color = Color.Unspecified) {
val finalColor = if (color == Color.Unspecified) { val finalColor = if (color == Color.Unspecified) {
if (isInDarkTheme()) MenuTextColorDark else Color.Black MenuTextColor
} else color } else color
DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) { DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
@@ -23,6 +23,7 @@ fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) {
Surface( Surface(
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
color = if (sent) sentColor else receivedColor, color = if (sent) sentColor else receivedColor,
contentColor = LocalContentColor.current
) { ) {
Row( Row(
Modifier.padding(horizontal = 12.dp, vertical = 6.dp), Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
@@ -56,6 +56,7 @@ fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) {
Modifier.clickable(onClick = onClick), Modifier.clickable(onClick = onClick),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
color = receivedColor, color = receivedColor,
contentColor = LocalContentColor.current
) { ) {
Row( Row(
Modifier.padding(horizontal = 12.dp, vertical = 6.dp), Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
@@ -26,6 +26,7 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: Mutabl
Surface( Surface(
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
color = if (ci.chatDir.sent) sentColor else receivedColor, color = if (ci.chatDir.sent) sentColor else receivedColor,
contentColor = LocalContentColor.current
) { ) {
Row( Row(
Modifier.padding(horizontal = 12.dp, vertical = 6.dp), Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
@@ -75,6 +75,8 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
SettingsView(chatModel, setPerformLA, scaffoldState.drawerState) SettingsView(chatModel, setPerformLA, scaffoldState.drawerState)
} }
}, },
contentColor = LocalContentColor.current,
drawerContentColor = LocalContentColor.current,
drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f), drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f),
drawerGesturesEnabled = appPlatform.isAndroid, drawerGesturesEnabled = appPlatform.isAndroid,
floatingActionButton = { floatingActionButton = {
@@ -30,6 +30,8 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
Scaffold( Scaffold(
Modifier.padding(end = endPadding), Modifier.padding(end = endPadding),
contentColor = LocalContentColor.current,
drawerContentColor = LocalContentColor.current,
scaffoldState = scaffoldState, scaffoldState = scaffoldState,
topBar = { Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } }, topBar = { Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } },
) { ) {
@@ -31,7 +31,6 @@ import chat.simplex.common.views.remote.*
import chat.simplex.common.views.usersettings.doWithAuth import chat.simplex.common.views.usersettings.doWithAuth
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -303,7 +302,7 @@ fun UserProfileRow(u: User) {
u.displayName, u.displayName,
modifier = Modifier modifier = Modifier
.padding(start = 10.dp, end = 8.dp), .padding(start = 10.dp, end = 8.dp),
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black, color = MenuTextColor,
fontWeight = if (u.activeUser) FontWeight.Medium else FontWeight.Normal fontWeight = if (u.activeUser) FontWeight.Medium else FontWeight.Normal
) )
} }
@@ -346,7 +345,7 @@ fun RemoteHostRow(h: RemoteHostInfo) {
Text( Text(
h.hostDeviceName, h.hostDeviceName,
modifier = Modifier.padding(start = 26.dp, end = 8.dp), modifier = Modifier.padding(start = 26.dp, end = 8.dp),
color = if (h.activeHost) MaterialTheme.colors.onBackground else if (isInDarkTheme()) MenuTextColorDark else Color.Black, color = if (h.activeHost) MaterialTheme.colors.onBackground else MenuTextColor,
fontSize = 14.sp, fontSize = 14.sp,
) )
} }
@@ -387,7 +386,7 @@ fun LocalDeviceRow(active: Boolean) {
Text( Text(
stringResource(MR.strings.this_device), stringResource(MR.strings.this_device),
modifier = Modifier.padding(start = 26.dp, end = 8.dp), modifier = Modifier.padding(start = 26.dp, end = 8.dp),
color = if (active) MaterialTheme.colors.onBackground else if (isInDarkTheme()) MenuTextColorDark else Color.Black, color = if (active) MaterialTheme.colors.onBackground else MenuTextColor,
fontSize = 14.sp, fontSize = 14.sp,
) )
} }
@@ -399,7 +398,7 @@ private fun UseFromDesktopPickerItem(onClick: () -> Unit) {
val text = generalGetString(MR.strings.settings_section_title_use_from_desktop).lowercase().capitalize(Locale.current) val text = generalGetString(MR.strings.settings_section_title_use_from_desktop).lowercase().capitalize(Locale.current)
Icon(painterResource(MR.images.ic_desktop), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) Icon(painterResource(MR.images.ic_desktop), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp)) Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black) Text(text, color = MenuTextColor)
} }
} }
@@ -409,7 +408,7 @@ private fun LinkAMobilePickerItem(onClick: () -> Unit) {
val text = generalGetString(MR.strings.link_a_mobile) val text = generalGetString(MR.strings.link_a_mobile)
Icon(painterResource(MR.images.ic_smartphone_300), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) Icon(painterResource(MR.images.ic_smartphone_300), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp)) Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black) Text(text, color = MenuTextColor)
} }
} }
@@ -419,7 +418,7 @@ private fun CreateInitialProfile(onClick: () -> Unit) {
val text = generalGetString(MR.strings.create_chat_profile) val text = generalGetString(MR.strings.create_chat_profile)
Icon(painterResource(MR.images.ic_manage_accounts), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) Icon(painterResource(MR.images.ic_manage_accounts), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp)) Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black) Text(text, color = MenuTextColor)
} }
} }
@@ -429,7 +428,7 @@ private fun SettingsPickerItem(onClick: () -> Unit) {
val text = generalGetString(MR.strings.settings_section_title_settings).lowercase().capitalize(Locale.current) val text = generalGetString(MR.strings.settings_section_title_settings).lowercase().capitalize(Locale.current)
Icon(painterResource(MR.images.ic_settings), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) Icon(painterResource(MR.images.ic_settings), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp)) Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black) Text(text, color = MenuTextColor)
} }
} }
@@ -439,7 +438,7 @@ private fun CancelPickerItem(onClick: () -> Unit) {
val text = generalGetString(MR.strings.cancel_verb) val text = generalGetString(MR.strings.cancel_verb)
Icon(painterResource(MR.images.ic_close), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) Icon(painterResource(MR.images.ic_close), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp)) Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black) Text(text, color = MenuTextColor)
} }
} }
@@ -366,7 +366,7 @@ fun chatArchiveTitle(chatArchiveTime: Instant, chatLastStart: Instant): String {
return stringResource(if (chatArchiveTime < chatLastStart) MR.strings.old_database_archive else MR.strings.new_database_archive) return stringResource(if (chatArchiveTime < chatLastStart) MR.strings.old_database_archive else MR.strings.new_database_archive)
} }
private fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) { fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) {
withApi { withApi {
try { try {
if (chatDbChanged.value) { if (chatDbChanged.value) {
@@ -406,6 +406,8 @@ private fun stopChatAlert(m: ChatModel) {
) )
} }
expect fun restartChatOrApp()
private fun exportProhibitedAlert() { private fun exportProhibitedAlert() {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.set_password_to_export), title = generalGetString(MR.strings.set_password_to_export),
@@ -413,7 +415,7 @@ private fun exportProhibitedAlert() {
) )
} }
private fun authStopChat(m: ChatModel) { fun authStopChat(m: ChatModel, onStop: (() -> Unit)? = null) {
if (m.controller.appPrefs.performLA.get()) { if (m.controller.appPrefs.performLA.get()) {
authenticate( authenticate(
generalGetString(MR.strings.auth_stop_chat), generalGetString(MR.strings.auth_stop_chat),
@@ -421,7 +423,7 @@ private fun authStopChat(m: ChatModel) {
completed = { laResult -> completed = { laResult ->
when (laResult) { when (laResult) {
LAResult.Success, is LAResult.Unavailable -> { LAResult.Success, is LAResult.Unavailable -> {
stopChat(m) stopChat(m, onStop)
} }
is LAResult.Error -> { is LAResult.Error -> {
m.chatRunning.value = true m.chatRunning.value = true
@@ -434,15 +436,16 @@ private fun authStopChat(m: ChatModel) {
} }
) )
} else { } else {
stopChat(m) stopChat(m, onStop)
} }
} }
private fun stopChat(m: ChatModel) { private fun stopChat(m: ChatModel, onStop: (() -> Unit)? = null) {
withApi { withApi {
try { try {
stopChatAsync(m) stopChatAsync(m)
platform.androidChatStopped() platform.androidChatStopped()
onStop?.invoke()
} catch (e: Error) { } catch (e: Error) {
m.chatRunning.value = true m.chatRunning.value = true
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_stopping_chat), e.toString()) AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_stopping_chat), e.toString())
@@ -460,10 +463,10 @@ suspend fun deleteChatAsync(m: ChatModel) {
m.controller.apiDeleteStorage() m.controller.apiDeleteStorage()
DatabaseUtils.ksDatabasePassword.remove() DatabaseUtils.ksDatabasePassword.remove()
m.controller.appPrefs.storeDBPassphrase.set(true) m.controller.appPrefs.storeDBPassphrase.set(true)
deleteChatDatabaseFiles() deleteAppDatabaseAndFiles()
} }
fun deleteChatDatabaseFiles() { fun deleteAppDatabaseAndFiles() {
val chat = File(dataDir, chatDatabaseFileName) val chat = File(dataDir, chatDatabaseFileName)
val chatBak = File(dataDir, "$chatDatabaseFileName.bak") val chatBak = File(dataDir, "$chatDatabaseFileName.bak")
val agent = File(dataDir, agentDatabaseFileName) val agent = File(dataDir, agentDatabaseFileName)
@@ -473,6 +476,7 @@ fun deleteChatDatabaseFiles() {
agent.delete() agent.delete()
agentBak.delete() agentBak.delete()
filesDir.deleteRecursively() filesDir.deleteRecursively()
filesDir.mkdir()
remoteHostsDir.deleteRecursively() remoteHostsDir.deleteRecursively()
tmpDir.deleteRecursively() tmpDir.deleteRecursively()
tmpDir.mkdir() tmpDir.mkdir()
@@ -152,7 +152,8 @@ fun CustomTimePickerDialog(
) { ) {
DefaultDialog(onDismissRequest = cancel) { DefaultDialog(onDismissRequest = cancel) {
Surface( Surface(
shape = RoundedCornerShape(corner = CornerSize(25.dp)) shape = RoundedCornerShape(corner = CornerSize(25.dp)),
contentColor = LocalContentColor.current
) { ) {
Box( Box(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@@ -17,7 +17,7 @@ object DatabaseUtils {
val ksAppPassword = KeyStoreItem(APP_PASSWORD_ALIAS, appPreferences.encryptedAppPassphrase, appPreferences.initializationVectorAppPassphrase) val ksAppPassword = KeyStoreItem(APP_PASSWORD_ALIAS, appPreferences.encryptedAppPassphrase, appPreferences.initializationVectorAppPassphrase)
val ksSelfDestructPassword = KeyStoreItem(SELF_DESTRUCT_PASSWORD_ALIAS, appPreferences.encryptedSelfDestructPassphrase, appPreferences.initializationVectorSelfDestructPassphrase) val ksSelfDestructPassword = KeyStoreItem(SELF_DESTRUCT_PASSWORD_ALIAS, appPreferences.encryptedSelfDestructPassphrase, appPreferences.initializationVectorSelfDestructPassphrase)
class KeyStoreItem(val alias: String, val passphrase: SharedPreference<String?>, val initVector: SharedPreference<String?>) { class KeyStoreItem(private val alias: String, val passphrase: SharedPreference<String?>, val initVector: SharedPreference<String?>) {
fun get(): String? { fun get(): String? {
return cryptor.decryptData( return cryptor.decryptData(
passphrase.get()?.toByteArrayFromBase64ForPassphrase() ?: return null, passphrase.get()?.toByteArrayFromBase64ForPassphrase() ?: return null,
@@ -70,7 +70,7 @@ fun <T> ExposedDropDownSetting(
selectionOption.second + (if (label != null) " $label" else ""), selectionOption.second + (if (label != null) " $label" else ""),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black, color = MenuTextColor,
fontSize = fontSize, fontSize = fontSize,
) )
} }
@@ -1,8 +1,7 @@
package chat.simplex.common.views.helpers package chat.simplex.common.views.helpers
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme import androidx.compose.material.*
import androidx.compose.material.Surface
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import chat.simplex.common.model.ChatController import chat.simplex.common.model.ChatController
import chat.simplex.common.model.ChatModel import chat.simplex.common.model.ChatModel
@@ -50,7 +49,7 @@ fun authenticateWithPasscode(
close() close()
completed(LAResult.Error(generalGetString(MR.strings.authentication_cancelled))) completed(LAResult.Error(generalGetString(MR.strings.authentication_cancelled)))
} }
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
LocalAuthView(ChatModel, LocalAuthRequest(promptTitle, promptSubtitle, password, selfDestruct && ChatController.appPrefs.selfDestruct.get()) { LocalAuthView(ChatModel, LocalAuthRequest(promptTitle, promptSubtitle, password, selfDestruct && ChatController.appPrefs.selfDestruct.get()) {
close() close()
completed(it) completed(it)
@@ -26,7 +26,7 @@ fun ModalView(
if (showClose) { if (showClose) {
BackHandler(onBack = close) BackHandler(onBack = close)
} }
Surface(Modifier.fillMaxSize()) { Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) {
Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) { Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) {
CloseSheetBar(close, showClose, endButtons) CloseSheetBar(close, showClose, endButtons)
Box(modifier) { content() } Box(modifier) { content() }
@@ -0,0 +1,64 @@
package chat.simplex.common.views.helpers
import chat.simplex.common.model.AgentErrorType
import chat.simplex.common.platform.Log
import chat.simplex.common.platform.TAG
import chat.simplex.common.platform.ntfManager
import chat.simplex.common.views.database.restartChatOrApp
import chat.simplex.res.MR
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
class ProcessedErrors <T: AgentErrorType>(val interval: Long) {
private var lastShownTimestamp: Long = -1
private var lastShownOfferRestart: Boolean = false
private var timer: Job = Job()
fun newError(error: T, offerRestart: Boolean) {
timer.cancel()
timer = withBGApi {
val delayBeforeNext = (lastShownTimestamp + interval) - System.currentTimeMillis()
if ((lastShownOfferRestart || !offerRestart) && delayBeforeNext >= 0) {
delay(delayBeforeNext)
}
lastShownTimestamp = System.currentTimeMillis()
lastShownOfferRestart = offerRestart
AlertManager.shared.hideAllAlerts()
showMessage(error, offerRestart)
}
}
private fun showMessage(error: T, offerRestart: Boolean) {
when (error) {
is AgentErrorType.CRITICAL -> {
val title = generalGetString(MR.strings.agent_critical_error_title)
val text = generalGetString(MR.strings.agent_critical_error_desc).format(error.criticalErr)
try {
ntfManager.showMessage(title, text)
} catch (e: Throwable) {
Log.e(TAG, e.stackTraceToString())
}
if (offerRestart) {
AlertManager.shared.showAlertDialog(
title = title,
text = text,
confirmText = generalGetString(MR.strings.restart_chat_button),
onConfirm = {
withApi { restartChatOrApp() }
})
} else {
AlertManager.shared.showAlertMsg(
title = title,
text = text,
)
}
}
is AgentErrorType.INTERNAL -> {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.agent_internal_error_title),
text = generalGetString(MR.strings.agent_internal_error_desc).format(error.internalErr),
)
}
}
}
}
@@ -34,7 +34,7 @@ fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) {
} else { } else {
val r: LAResult = if (passcode.value == authRequest.password) { val r: LAResult = if (passcode.value == authRequest.password) {
if (authRequest.selfDestruct && sdPassword != null && controller.ctrl == -1L) { if (authRequest.selfDestruct && sdPassword != null && controller.ctrl == -1L) {
initChatControllerAndRunMigrations(true) initChatControllerAndRunMigrations()
} }
LAResult.Success LAResult.Success
} else { } else {
@@ -67,8 +67,8 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (
* */ * */
chatCloseStore(ctrl) chatCloseStore(ctrl)
} }
deleteChatDatabaseFiles() deleteAppDatabaseAndFiles()
// Clear sensitive data on screen just in case ModalManager will fail to prevent hiding its modals while database encrypts itself // Clear sensitive data on screen just in case ModalManager fails to hide its modals while new database is created
m.chatId.value = null m.chatId.value = null
m.chatItems.clear() m.chatItems.clear()
m.chats.clear() m.chats.clear()
@@ -12,6 +12,7 @@ import chat.simplex.res.MR
@Composable @Composable
fun SetAppPasscodeView( fun SetAppPasscodeView(
passcodeKeychain: DatabaseUtils.KeyStoreItem = ksAppPassword, passcodeKeychain: DatabaseUtils.KeyStoreItem = ksAppPassword,
prohibitedPasscodeKeychain: DatabaseUtils.KeyStoreItem = ksSelfDestructPassword,
title: String = generalGetString(MR.strings.new_passcode), title: String = generalGetString(MR.strings.new_passcode),
reason: String? = null, reason: String? = null,
submit: () -> Unit, submit: () -> Unit,
@@ -51,7 +52,7 @@ fun SetAppPasscodeView(
} else { } else {
SetPasswordView(title, generalGetString(MR.strings.save_verb), SetPasswordView(title, generalGetString(MR.strings.save_verb),
// Do not allow to set app passcode == selfDestruct passcode // Do not allow to set app passcode == selfDestruct passcode
submitEnabled = { pwd -> pwd != (if (passcodeKeychain.alias == ksSelfDestructPassword.alias) ksAppPassword else ksSelfDestructPassword).get() }) { submitEnabled = { pwd -> pwd != prohibitedPasscodeKeychain.get() }) {
enteredPassword = passcode.value enteredPassword = passcode.value
passcode.value = "" passcode.value = ""
confirming = true confirming = true
@@ -175,7 +175,7 @@ fun ActionButton(
disabled: Boolean = false, disabled: Boolean = false,
click: () -> Unit = {} click: () -> Unit = {}
) { ) {
Surface(shape = RoundedCornerShape(18.dp), color = Color.Transparent) { Surface(shape = RoundedCornerShape(18.dp), color = Color.Transparent, contentColor = LocalContentColor.current) {
Column( Column(
Modifier Modifier
.clickable(onClick = click) .clickable(onClick = click)
@@ -220,7 +220,7 @@ fun ActionButton(
disabled: Boolean = false, disabled: Boolean = false,
click: () -> Unit = {} click: () -> Unit = {}
) { ) {
Surface(modifier, shape = RoundedCornerShape(18.dp)) { Surface(modifier, shape = RoundedCornerShape(18.dp), contentColor = LocalContentColor.current) {
Column( Column(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -380,7 +380,8 @@ fun SettingsSectionFooter(revert: () -> Unit, save: () -> Unit, disabled: Boolea
fun FooterButton(icon: Painter, title: String, action: () -> Unit, disabled: Boolean) { fun FooterButton(icon: Painter, title: String, action: () -> Unit, disabled: Boolean) {
Surface( Surface(
shape = RoundedCornerShape(20.dp), shape = RoundedCornerShape(20.dp),
color = Color.Black.copy(alpha = 0f) color = Color.Black.copy(alpha = 0f),
contentColor = LocalContentColor.current
) { ) {
val modifier = if (disabled) Modifier else Modifier.clickable { action() } val modifier = if (disabled) Modifier else Modifier.clickable { action() }
Row( Row(
@@ -10,10 +10,11 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import chat.simplex.common.model.*
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.appPlatform import chat.simplex.common.platform.appPlatform
import chat.simplex.common.platform.appPreferences
import chat.simplex.common.views.TerminalView import chat.simplex.common.views.TerminalView
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR import chat.simplex.res.MR
@@ -44,6 +45,7 @@ fun DeveloperView(
m.controller.appPrefs.terminalAlwaysVisible.set(false) m.controller.appPrefs.terminalAlwaysVisible.set(false)
} }
} }
SettingsPreferenceItem(painterResource(MR.images.ic_report), stringResource(MR.strings.show_internal_errors), appPreferences.showInternalErrors)
} }
} }
SectionTextFooter( SectionTextFooter(
@@ -383,7 +383,7 @@ fun SimplexLockView(
} }
LAMode.PASSCODE -> { LAMode.PASSCODE -> {
ModalManager.fullscreen.showCustomModal { close -> ModalManager.fullscreen.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
SetAppPasscodeView( SetAppPasscodeView(
submit = { submit = {
laLockDelay.set(30) laLockDelay.set(30)
@@ -427,7 +427,7 @@ fun SimplexLockView(
when (laResult) { when (laResult) {
LAResult.Success -> { LAResult.Success -> {
ModalManager.fullscreen.showCustomModal { close -> ModalManager.fullscreen.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
SetAppPasscodeView( SetAppPasscodeView(
reason = generalGetString(MR.strings.la_app_passcode), reason = generalGetString(MR.strings.la_app_passcode),
submit = { submit = {
@@ -451,9 +451,10 @@ fun SimplexLockView(
when (laResult) { when (laResult) {
LAResult.Success -> { LAResult.Success -> {
ModalManager.fullscreen.showCustomModal { close -> ModalManager.fullscreen.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
SetAppPasscodeView( SetAppPasscodeView(
passcodeKeychain = ksSelfDestructPassword, passcodeKeychain = ksSelfDestructPassword,
prohibitedPasscodeKeychain = ksAppPassword,
reason = generalGetString(MR.strings.self_destruct), reason = generalGetString(MR.strings.self_destruct),
submit = { submit = {
selfDestructPasscodeAlert(generalGetString(MR.strings.self_destruct_passcode_changed)) selfDestructPasscodeAlert(generalGetString(MR.strings.self_destruct_passcode_changed))
@@ -487,7 +488,7 @@ fun SimplexLockView(
} }
LAMode.PASSCODE -> { LAMode.PASSCODE -> {
ModalManager.fullscreen.showCustomModal { close -> ModalManager.fullscreen.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
SetAppPasscodeView( SetAppPasscodeView(
submit = { submit = {
laLockDelay.set(30) laLockDelay.set(30)
@@ -598,9 +599,9 @@ private fun EnableSelfDestruct(
selfDestruct: SharedPreference<Boolean>, selfDestruct: SharedPreference<Boolean>,
close: () -> Unit close: () -> Unit
) { ) {
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
SetAppPasscodeView( SetAppPasscodeView(
passcodeKeychain = ksSelfDestructPassword, title = generalGetString(MR.strings.set_passcode), reason = generalGetString(MR.strings.enabled_self_destruct_passcode), passcodeKeychain = ksSelfDestructPassword, prohibitedPasscodeKeychain = ksAppPassword, title = generalGetString(MR.strings.set_passcode), reason = generalGetString(MR.strings.enabled_self_destruct_passcode),
submit = { submit = {
selfDestruct.set(true) selfDestruct.set(true)
selfDestructPasscodeAlert(generalGetString(MR.strings.self_destruct_passcode_enabled)) selfDestructPasscodeAlert(generalGetString(MR.strings.self_destruct_passcode_enabled))
@@ -155,7 +155,8 @@ fun RTCServersLayout(
.height(160.dp) .height(160.dp)
.fillMaxWidth(), .fillMaxWidth(),
shape = RoundedCornerShape(10.dp), shape = RoundedCornerShape(10.dp),
border = BorderStroke(1.dp, MaterialTheme.colors.secondaryVariant) border = BorderStroke(1.dp, MaterialTheme.colors.secondaryVariant),
contentColor = LocalContentColor.current
) { ) {
SelectionContainer( SelectionContainer(
Modifier.verticalScroll(rememberScrollState()) Modifier.verticalScroll(rememberScrollState())
@@ -155,7 +155,7 @@ fun UserAddressView(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
if (userAddress.value != null) { if (userAddress.value != null) {
Surface(Modifier.size(50.dp), color = MaterialTheme.colors.background.copy(0.9f), shape = RoundedCornerShape(50)){} Surface(Modifier.size(50.dp), color = MaterialTheme.colors.background.copy(0.9f), contentColor = LocalContentColor.current, shape = RoundedCornerShape(50)){}
} }
CircularProgressIndicator( CircularProgressIndicator(
Modifier Modifier
@@ -90,6 +90,8 @@
<string name="failed_to_create_user_title">Error creating profile!</string> <string name="failed_to_create_user_title">Error creating profile!</string>
<string name="failed_to_create_user_duplicate_title">Duplicate display name!</string> <string name="failed_to_create_user_duplicate_title">Duplicate display name!</string>
<string name="failed_to_create_user_duplicate_desc">You already have a chat profile with the same display name. Please choose another name.</string> <string name="failed_to_create_user_duplicate_desc">You already have a chat profile with the same display name. Please choose another name.</string>
<string name="failed_to_create_user_invalid_title">Invalid display name!</string>
<string name="failed_to_create_user_invalid_desc">This display name is invalid. Please choose another name.</string>
<string name="failed_to_active_user_title">Error switching profile!</string> <string name="failed_to_active_user_title">Error switching profile!</string>
<!-- API Error Responses - SimpleXAPI.kt --> <!-- API Error Responses - SimpleXAPI.kt -->
@@ -684,6 +686,7 @@
<string name="hide_dev_options">Hide:</string> <string name="hide_dev_options">Hide:</string>
<string name="show_developer_options">Show developer options</string> <string name="show_developer_options">Show developer options</string>
<string name="developer_options">Database IDs and Transport isolation option.</string> <string name="developer_options">Database IDs and Transport isolation option.</string>
<string name="show_internal_errors">Show internal errors</string>
<string name="shutdown_alert_question">Shutdown?</string> <string name="shutdown_alert_question">Shutdown?</string>
<string name="shutdown_alert_desc">Notifications will stop working until you re-launch the app</string> <string name="shutdown_alert_desc">Notifications will stop working until you re-launch the app</string>
@@ -1693,6 +1696,10 @@
<string name="disconnect_remote_host">Disconnect</string> <string name="disconnect_remote_host">Disconnect</string>
<string name="disconnect_remote_hosts">Disconnect mobiles</string> <string name="disconnect_remote_hosts">Disconnect mobiles</string>
<string name="remote_host_was_disconnected_toast"><![CDATA[Mobile <b>%s</b> was disconnected]]></string> <string name="remote_host_was_disconnected_toast"><![CDATA[Mobile <b>%s</b> was disconnected]]></string>
<string name="remote_host_was_disconnected_title">Connection stopped</string>
<string name="remote_ctrl_was_disconnected_title">Connection stopped</string>
<string name="remote_host_disconnected_from"><![CDATA[Disconnected from mobile <b>%s</b> with the reason: %s]]></string>
<string name="remote_ctrl_disconnected_with_reason">Disconnected with the reason: %s</string>
<string name="disconnect_desktop_question">Disconnect desktop?</string> <string name="disconnect_desktop_question">Disconnect desktop?</string>
<string name="only_one_device_can_work_at_the_same_time">Only one device can work at the same time</string> <string name="only_one_device_can_work_at_the_same_time">Only one device can work at the same time</string>
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[Open <i>Use from desktop</i> in mobile app and scan QR code.]]></string> <string name="open_on_mobile_and_scan_qr_code"><![CDATA[Open <i>Use from desktop</i> in mobile app and scan QR code.]]></string>
@@ -1727,6 +1734,20 @@
<string name="random_port">Random</string> <string name="random_port">Random</string>
<string name="open_port_in_firewall_title">Open port in firewall</string> <string name="open_port_in_firewall_title">Open port in firewall</string>
<string name="open_port_in_firewall_desc">To allow a mobile app to connect to the desktop, open this port in your firewall, if you have it enabled</string> <string name="open_port_in_firewall_desc">To allow a mobile app to connect to the desktop, open this port in your firewall, if you have it enabled</string>
<string name="remote_host_error_missing"><![CDATA[Mobile <b>%s</b> is missing]]></string>
<string name="remote_host_error_inactive"><![CDATA[Mobile <b>%s</b> is inactive]]></string>
<string name="remote_host_error_busy"><![CDATA[Mobile <b>%s</b> is busy]]></string>
<string name="remote_host_error_timeout"><![CDATA[Timeout reached while connecting to the mobile <b>%s</b>]]></string>
<string name="remote_host_error_bad_state"><![CDATA[Connection to the mobile <b>%s</b> is in a bad state]]></string>
<string name="remote_host_error_bad_version"><![CDATA[Mobile <b>%s</b> has an unsupported version. Please, make sure you use the same version on both devices]]></string>
<string name="remote_host_error_disconnected"><![CDATA[Mobile <b>%s</b> was disconnected]]></string>
<string name="remote_ctrl_error_inactive">Desktop is inactive</string>
<string name="remote_ctrl_error_bad_state">Connection to the desktop is in a bad state</string>
<string name="remote_ctrl_error_busy">Desktop is busy</string>
<string name="remote_ctrl_error_timeout">Timeout reached while connecting to the desktop</string>
<string name="remote_ctrl_error_disconnected">Desktop was disconnected</string>
<string name="remote_ctrl_error_bad_invitation">Desktop has wrong invitation code</string>
<string name="remote_ctrl_error_bad_version">Desktop has an unsupported version. Please, make sure you use the same version on both devices</string>
<!-- Under development --> <!-- Under development -->
<string name="in_developing_title">Coming soon!</string> <string name="in_developing_title">Coming soon!</string>
@@ -1751,4 +1772,11 @@
<string name="connect_plan_you_are_already_joining_the_group_via_this_link">You are already joining the group via this link.</string> <string name="connect_plan_you_are_already_joining_the_group_via_this_link">You are already joining the group via this link.</string>
<string name="connect_plan_you_are_already_in_group_vName"><![CDATA[You are already in group <b>%1$s</b>.]]></string> <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[You are already in group <b>%1$s</b>.]]></string>
<string name="connect_plan_connect_via_link">Connect via link?</string> <string name="connect_plan_connect_via_link">Connect via link?</string>
<!-- Errors -->
<string name="agent_critical_error_title">Critical error</string>
<string name="agent_critical_error_desc">Please report it to the developers: \n%s\n\nIt is recommended to restart the app.</string>
<string name="agent_internal_error_title">Internal error</string>
<string name="agent_internal_error_desc">Please report it to the developers: \n%s</string>
<string name="restart_chat_button">Restart chat</string>
</resources> </resources>
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M479.895-284Q494-284 504-293.895q10-9.894 10-24Q514-332 504.105-342q-9.894-10-24-10Q466-352 456-342.105q-10 9.894-10 24Q446-304 455.895-294q9.894 10 24 10ZM451.5-425H509v-261h-57.5v261ZM332-124.5 124.5-332.176V-628l207.676-207.5H628l207.5 207.676V-332L627.824-124.5H332Zm24.222-57.5h248.243L778-356.222v-248.243L604.242-778H356L182-604.242V-356l174.222 174ZM480-480Z"/></svg>

After

Width:  |  Height:  |  Size: 472 B

@@ -16,8 +16,8 @@ import javax.imageio.ImageIO
object NtfManager { object NtfManager {
private val prevNtfs = arrayListOf<Pair<ChatId, Slice>>() private val prevNtfs = arrayListOf<Pair<ChatId, Slice>>()
fun notifyCallInvitation(invitation: RcvCallInvitation) { fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean {
if (simplexWindowState.windowFocused.value) return if (simplexWindowState.windowFocused.value) return false
val contactId = invitation.contact.id val contactId = invitation.contact.id
Log.d(TAG, "notifyCallInvitation $contactId") Log.d(TAG, "notifyCallInvitation $contactId")
val image = invitation.contact.image val image = invitation.contact.image
@@ -45,6 +45,11 @@ object NtfManager {
displayNotificationViaLib(contactId, title, text, prepareIconPath(largeIcon), actions) { displayNotificationViaLib(contactId, title, text, prepareIconPath(largeIcon), actions) {
ntfManager.openChatAction(invitation.user.userId, contactId) ntfManager.openChatAction(invitation.user.userId, contactId)
} }
return true
}
fun showMessage(title: String, text: String) {
displayNotificationViaLib("MESSAGE", title, text, null, emptyList()) {}
} }
fun hasNotificationsForChat(chatId: ChatId) = false//prevNtfs.any { it.first == chatId } fun hasNotificationsForChat(chatId: ChatId) = false//prevNtfs.any { it.first == chatId }
@@ -15,16 +15,19 @@ val defaultLocale: Locale = Locale.getDefault()
fun initApp() { fun initApp() {
ntfManager = object : NtfManager() { ntfManager = object : NtfManager() {
override fun notifyCallInvitation(invitation: RcvCallInvitation) = chat.simplex.common.model.NtfManager.notifyCallInvitation(invitation) override fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean = chat.simplex.common.model.NtfManager.notifyCallInvitation(invitation)
override fun hasNotificationsForChat(chatId: String): Boolean = chat.simplex.common.model.NtfManager.hasNotificationsForChat(chatId) override fun hasNotificationsForChat(chatId: String): Boolean = chat.simplex.common.model.NtfManager.hasNotificationsForChat(chatId)
override fun cancelNotificationsForChat(chatId: String) = chat.simplex.common.model.NtfManager.cancelNotificationsForChat(chatId) override fun cancelNotificationsForChat(chatId: String) = chat.simplex.common.model.NtfManager.cancelNotificationsForChat(chatId)
override fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String?, actions: List<Pair<NotificationAction, () -> Unit>>) = chat.simplex.common.model.NtfManager.displayNotification(user, chatId, displayName, msgText, image, actions) override fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String?, actions: List<Pair<NotificationAction, () -> Unit>>) = chat.simplex.common.model.NtfManager.displayNotification(user, chatId, displayName, msgText, image, actions)
override fun androidCreateNtfChannelsMaybeShowAlert() {} override fun androidCreateNtfChannelsMaybeShowAlert() {}
override fun cancelCallNotification() {} override fun cancelCallNotification() {}
override fun cancelAllNotifications() = chat.simplex.common.model.NtfManager.cancelAllNotifications() override fun cancelAllNotifications() = chat.simplex.common.model.NtfManager.cancelAllNotifications()
override fun showMessage(title: String, text: String) = chat.simplex.common.model.NtfManager.showMessage(title, text)
} }
applyAppLocale() applyAppLocale()
initChatControllerAndRunMigrations(false) if (DatabaseUtils.ksSelfDestructPassword.get() == null) {
initChatControllerAndRunMigrations()
}
// LALAL // LALAL
//testCrypto() //testCrypto()
} }
@@ -167,7 +167,8 @@ actual fun PlatformTextField(
decorationBox = { innerTextField -> decorationBox = { innerTextField ->
Surface( Surface(
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
border = BorderStroke(1.dp, MaterialTheme.colors.secondary) border = BorderStroke(1.dp, MaterialTheme.colors.secondary),
contentColor = LocalContentColor.current
) { ) {
Row( Row(
Modifier.background(MaterialTheme.colors.background), Modifier.background(MaterialTheme.colors.background),
@@ -0,0 +1,23 @@
package chat.simplex.common.views.database
import androidx.compose.runtime.mutableStateOf
import chat.simplex.common.platform.chatModel
import chat.simplex.common.views.helpers.withApi
import kotlinx.coroutines.delay
import kotlinx.datetime.Instant
actual fun restartChatOrApp() {
if (chatModel.chatRunning.value == false) {
chatModel.chatDbChanged.value = true
startChat(chatModel, mutableStateOf(Instant.DISTANT_PAST), chatModel.chatDbChanged)
} else {
authStopChat(chatModel) {
withApi {
// adding delay in order to prevent locked database by previous initialization
delay(1000)
chatModel.chatDbChanged.value = true
startChat(chatModel, mutableStateOf(Instant.DISTANT_PAST), chatModel.chatDbChanged)
}
}
}
}
@@ -2,8 +2,7 @@ package chat.simplex.common.views.helpers
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme import androidx.compose.material.*
import androidx.compose.material.Surface
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.* import androidx.compose.ui.input.key.*
@@ -39,7 +38,8 @@ actual fun DefaultDialog(
) { ) {
Surface( Surface(
Modifier Modifier
.border(border = BorderStroke(1.dp, MaterialTheme.colors.secondary.copy(alpha = 0.3F)), shape = RoundedCornerShape(8)) .border(border = BorderStroke(1.dp, MaterialTheme.colors.secondary.copy(alpha = 0.3F)), shape = RoundedCornerShape(8)),
contentColor = LocalContentColor.current
) { ) {
content() content()
} }
+1 -1
View File
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package source-repository-package
type: git type: git
location: https://github.com/simplex-chat/simplexmq.git location: https://github.com/simplex-chat/simplexmq.git
tag: d0588bd0ac23a459cbfc9a4789633014e91ffa19 tag: ad8cd1d5154617663065652b45c784ad5a0a584d
source-repository-package source-repository-package
type: git type: git
+1 -1
View File
@@ -1,5 +1,5 @@
name: simplex-chat name: simplex-chat
version: 5.5.0.0 version: 5.5.0.1
#synopsis: #synopsis:
#description: #description:
homepage: https://github.com/simplex-chat/simplex-chat#readme homepage: https://github.com/simplex-chat/simplex-chat#readme
+19 -4
View File
@@ -8,9 +8,9 @@ u="$USER"
tmp="$(mktemp -d -t)" tmp="$(mktemp -d -t)"
folder="$tmp/simplex-chat" folder="$tmp/simplex-chat"
nix_ver="nix-2.15.1" nix_ver="nix-2.19.2"
nix_url="https://releases.nixos.org/nix/$nix_ver/install" nix_url="https://releases.nixos.org/nix/$nix_ver/install"
nix_hash="67aa37f0115195d8ddf32b5d6f471f1e60ecca0fdb3e98bcf54bc147c3078640" nix_hash="435f0d7e11f7c7dffeeab0ec9cc55723f6d3c03352379d785633cf4ddb5caf90"
nix_config="sandbox = true nix_config="sandbox = true
max-jobs = auto max-jobs = auto
experimental-features = nix-command flakes" experimental-features = nix-command flakes"
@@ -102,8 +102,19 @@ build() {
sed -i.bak '/android {/a lint {abortOnError = false}' "$folder/apps/multiplatform/android/build.gradle.kts" sed -i.bak '/android {/a lint {abortOnError = false}' "$folder/apps/multiplatform/android/build.gradle.kts"
for arch in $arches; do for arch in $arches; do
android_simplex_lib="${folder}#hydraJobs.${arch}-android:lib:simplex-chat.x86_64-linux"
android_support_lib="${folder}#hydraJobs.${arch}-android:lib:support.x86_64-linux" tag_full="$(git tag --points-at HEAD)"
tag_version="${tag_full%%-*}"
if [ "$arch" = "armv7a" ] && [ -n "$tag_full" ] ; then
git checkout "${tag_version}-armv7a"
android_simplex_lib="${folder}#hydraJobs.${arch}-android:lib:simplex-chat.x86_64-linux"
android_support_lib="${folder}#hydraJobs.${arch}-android:lib:support.x86_64-linux"
else
android_simplex_lib="${folder}#hydraJobs.x86_64-linux.${arch}-android:lib:simplex-chat"
android_support_lib="${folder}#hydraJobs.x86_64-linux.${arch}-android:lib:support"
fi
android_simplex_lib_output="${PWD}/result/pkg-${arch}-android-libsimplex.zip" android_simplex_lib_output="${PWD}/result/pkg-${arch}-android-libsimplex.zip"
android_support_lib_output="${PWD}/result/pkg-${arch}-android-libsupport.zip" android_support_lib_output="${PWD}/result/pkg-${arch}-android-libsupport.zip"
@@ -139,6 +150,10 @@ build() {
zipalign -p -f 4 "$tmp/$android_apk_output_final" "$PWD/$android_apk_output_final" zipalign -p -f 4 "$tmp/$android_apk_output_final" "$PWD/$android_apk_output_final"
rm -rf "$libs_folder/$android_arch" rm -rf "$libs_folder/$android_arch"
if [ "$arch" = "armv7a" ] && [ -n "$tag_full" ] ; then
git checkout "${tag_full}"
fi
done done
} }
+2 -2
View File
@@ -37,12 +37,12 @@ for ((i = 0 ; i < ${#arches[@]}; i++)); do
mkdir -p "$output_dir" 2> /dev/null mkdir -p "$output_dir" 2> /dev/null
curl --location -o libsupport.zip $job_repo/$arch-android:lib:support.x86_64-linux/latest/download/1 && \ curl --location -o libsupport.zip $job_repo/x86_64-linux."$arch"-android:lib:support/latest/download/1 && \
unzip -o libsupport.zip && \ unzip -o libsupport.zip && \
mv libsupport.so "$output_dir" && \ mv libsupport.so "$output_dir" && \
rm libsupport.zip rm libsupport.zip
curl --location -o libsimplex.zip "$job_repo"/"$arch"-android:lib:simplex-chat.x86_64-linux/latest/download/1 && \ curl --location -o libsimplex.zip "$job_repo"/x86_64-linux."$arch"-android:lib:simplex-chat/latest/download/1 && \
unzip -o libsimplex.zip && \ unzip -o libsimplex.zip && \
mv libsimplex.so "$output_dir" && \ mv libsimplex.so "$output_dir" && \
rm libsimplex.zip rm libsimplex.zip
+2 -2
View File
@@ -35,7 +35,7 @@ for ((i = 0 ; i < ${#arches[@]}; i++)); do
output_arch="${output_arches[$i]}" output_arch="${output_arches[$i]}"
output_dir="$HOME/Downloads" output_dir="$HOME/Downloads"
curl --location -o "$output_dir"/pkg-ios-"$arch"-swift-json.zip "$job_repo"/"$arch"-darwin-ios:lib:simplex-chat."$arch"-darwin/latest/download/1 && \ curl --location -o "$output_dir"/pkg-ios-"$arch"-swift-json.zip "$job_repo"/"$arch"-darwin."$arch"-darwin-ios:lib:simplex-chat/latest/download/1 && \
unzip -o "$output_dir"/pkg-ios-"$output_arch"-swift-json.zip -d ~/Downloads/pkg-ios-"$output_arch"-swift-json unzip -o "$output_dir"/pkg-ios-"$output_arch"-swift-json.zip -d ~/Downloads/pkg-ios-"$output_arch"-swift-json
done done
sh "$root_dir"/scripts/ios/prepare-x86_64.sh sh "$root_dir"/scripts/ios/prepare-x86_64.sh
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"https://github.com/simplex-chat/simplexmq.git"."d0588bd0ac23a459cbfc9a4789633014e91ffa19" = "0b17qy74capb0jyli8f3pg1xi4aawhcgpmaz2ykl9g3605png1na"; "https://github.com/simplex-chat/simplexmq.git"."ad8cd1d5154617663065652b45c784ad5a0a584d" = "19sinz1gynab776x8h9va7r6ifm9pmgzljsbc7z5cbkcnjl5sfh3";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "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/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
+1 -1
View File
@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack -- see: https://github.com/sol/hpack
name: simplex-chat name: simplex-chat
version: 5.5.0.0 version: 5.5.0.1
category: Web, System, Services, Cryptography category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat author: simplex.chat
+224 -135
View File
@@ -24,7 +24,7 @@ import Control.Monad.Reader
import qualified Data.Aeson as J import qualified Data.Aeson as J
import Data.Attoparsec.ByteString.Char8 (Parser) import Data.Attoparsec.ByteString.Char8 (Parser)
import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Bifunctor (bimap, first) import Data.Bifunctor (bimap, first, second)
import Data.ByteArray (ScrubbedBytes) import Data.ByteArray (ScrubbedBytes)
import qualified Data.ByteArray as BA import qualified Data.ByteArray as BA
import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Base64 as B64
@@ -37,6 +37,7 @@ import Data.Constraint (Dict (..))
import Data.Either (fromRight, lefts, partitionEithers, rights) import Data.Either (fromRight, lefts, partitionEithers, rights)
import Data.Fixed (div') import Data.Fixed (div')
import Data.Functor (($>)) import Data.Functor (($>))
import Data.Functor.Identity
import Data.Int (Int64) import Data.Int (Int64)
import Data.List (find, foldl', isSuffixOf, partition, sortOn) import Data.List (find, foldl', isSuffixOf, partition, sortOn)
import Data.List.NonEmpty (NonEmpty (..), nonEmpty, toList, (<|)) import Data.List.NonEmpty (NonEmpty (..), nonEmpty, toList, (<|))
@@ -87,6 +88,7 @@ import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentCl
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig)
import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Lock
import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Protocol
import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..))
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, SQLiteStore (dbNew), execSQL, upMigration, withConnection) import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, SQLiteStore (dbNew), execSQL, upMigration, withConnection)
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
@@ -233,6 +235,7 @@ newChatController
expireCIFlags <- newTVarIO M.empty expireCIFlags <- newTVarIO M.empty
cleanupManagerAsync <- newTVarIO Nothing cleanupManagerAsync <- newTVarIO Nothing
timedItemThreads <- atomically TM.empty timedItemThreads <- atomically TM.empty
chatActivated <- newTVarIO True
showLiveItems <- newTVarIO False showLiveItems <- newTVarIO False
encryptLocalFiles <- newTVarIO False encryptLocalFiles <- newTVarIO False
userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg
@@ -268,6 +271,7 @@ newChatController
expireCIFlags, expireCIFlags,
cleanupManagerAsync, cleanupManagerAsync,
timedItemThreads, timedItemThreads,
chatActivated,
showLiveItems, showLiveItems,
encryptLocalFiles, encryptLocalFiles,
userXFTPFileConfig, userXFTPFileConfig,
@@ -311,10 +315,10 @@ cfgServers p DefaultAgentServers {smp, xftp} = case p of
SPSMP -> smp SPSMP -> smp
SPXFTP -> xftp SPXFTP -> xftp
startChatController :: forall m. ChatMonad' m => Bool -> Bool -> Bool -> m (Async ()) startChatController :: forall m. ChatMonad' m => Bool -> m (Async ())
startChatController subConns enableExpireCIs startXFTPWorkers = do startChatController mainApp = do
asks smpAgent >>= resumeAgentClient asks smpAgent >>= resumeAgentClient
unless subConns $ unless mainApp $
chatWriteVar subscriptionMode SMOnlyCreate chatWriteVar subscriptionMode SMOnlyCreate
users <- fromRight [] <$> runExceptT (withStoreCtx' (Just "startChatController, getUsers") getUsers) users <- fromRight [] <$> runExceptT (withStoreCtx' (Just "startChatController, getUsers") getUsers)
restoreCalls restoreCalls
@@ -324,15 +328,15 @@ startChatController subConns enableExpireCIs startXFTPWorkers = do
start s users = do start s users = do
a1 <- async agentSubscriber a1 <- async agentSubscriber
a2 <- a2 <-
if subConns if mainApp
then Just <$> async (subscribeUsers False users) then Just <$> async (subscribeUsers False users)
else pure Nothing else pure Nothing
atomically . writeTVar s $ Just (a1, a2) atomically . writeTVar s $ Just (a1, a2)
when startXFTPWorkers $ do when mainApp $ do
startXFTP startXFTP
void $ forkIO $ startFilesToReceive users void $ forkIO $ startFilesToReceive users
startCleanupManager startCleanupManager
when enableExpireCIs $ startExpireCIs users startExpireCIs users
pure a1 pure a1
startXFTP = do startXFTP = do
tmp <- readTVarIO =<< asks tempDirectory tmp <- readTVarIO =<< asks tempDirectory
@@ -454,8 +458,9 @@ processChatCommand' vr = \case
withStore' getUsers >>= \case withStore' getUsers >>= \case
[] -> pure 1 [] -> pure 1
users -> do users -> do
when (any (\User {localDisplayName = n} -> n == displayName) users) $ forM_ users $ \User {localDisplayName = n, activeUser, viewPwdHash} ->
throwChatError (CEUserExists displayName) when (n == displayName) . throwChatError $
if activeUser || isNothing viewPwdHash then CEUserExists displayName else CEInvalidDisplayName {displayName, validName = ""}
withAgent (\a -> createUser a smp xftp) withAgent (\a -> createUser a smp xftp)
ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure
user <- withStore $ \db -> createUserRecordAt db (AgentUserId auId) p True ts user <- withStore $ \db -> createUserRecordAt db (AgentUserId auId) p True ts
@@ -544,16 +549,17 @@ processChatCommand' vr = \case
checkDeleteChatUser user' checkDeleteChatUser user'
withChatLock "deleteUser" . procCmd $ deleteChatUser user' delSMPQueues withChatLock "deleteUser" . procCmd $ deleteChatUser user' delSMPQueues
DeleteUser uName delSMPQueues viewPwd_ -> withUserName uName $ \userId -> APIDeleteUser userId delSMPQueues viewPwd_ DeleteUser uName delSMPQueues viewPwd_ -> withUserName uName $ \userId -> APIDeleteUser userId delSMPQueues viewPwd_
StartChat subConns enableExpireCIs startXFTPWorkers -> withUser' $ \_ -> StartChat mainApp -> withUser' $ \_ ->
asks agentAsync >>= readTVarIO >>= \case asks agentAsync >>= readTVarIO >>= \case
Just _ -> pure CRChatRunning Just _ -> pure CRChatRunning
_ -> checkStoreNotChanged $ startChatController subConns enableExpireCIs startXFTPWorkers $> CRChatStarted _ -> checkStoreNotChanged $ startChatController mainApp $> CRChatStarted
APIStopChat -> do APIStopChat -> do
ask >>= stopChatController ask >>= stopChatController
pure CRChatStopped pure CRChatStopped
APIActivateChat restoreChat -> withUser $ \_ -> do APIActivateChat restoreChat -> withUser $ \_ -> do
when restoreChat restoreCalls when restoreChat restoreCalls
withAgent foregroundAgent withAgent foregroundAgent
chatWriteVar chatActivated True
when restoreChat $ do when restoreChat $ do
users <- withStoreCtx' (Just "APIActivateChat, getUsers") getUsers users <- withStoreCtx' (Just "APIActivateChat, getUsers") getUsers
void . forkIO $ subscribeUsers True users void . forkIO $ subscribeUsers True users
@@ -561,6 +567,7 @@ processChatCommand' vr = \case
setAllExpireCIFlags True setAllExpireCIFlags True
ok_ ok_
APISuspendChat t -> do APISuspendChat t -> do
chatWriteVar chatActivated False
setAllExpireCIFlags False setAllExpireCIFlags False
stopRemoteCtrl stopRemoteCtrl
withAgent (`suspendAgent` t) withAgent (`suspendAgent` t)
@@ -2135,31 +2142,41 @@ processChatCommand' vr = \case
| otherwise = do | otherwise = do
when (n /= n') $ checkValidName n' when (n /= n') $ checkValidName n'
-- read contacts before user update to correctly merge preferences -- read contacts before user update to correctly merge preferences
-- [incognito] filter out contacts with whom user has incognito connections contacts <- withStore' (`getUserContacts` user)
contacts <-
filter (\ct -> contactReady ct && contactActive ct && not (contactConnIncognito ct))
<$> withStore' (`getUserContacts` user)
user' <- updateUser user' <- updateUser
asks currentUser >>= atomically . (`writeTVar` Just user') asks currentUser >>= atomically . (`writeTVar` Just user')
withChatLock "updateProfile" . procCmd $ do withChatLock "updateProfile" . procCmd $ do
ChatConfig {logLevel} <- asks config let changedCts = foldr (addChangedProfileContact user') [] contacts
summary <- foldM (processAndCount user' logLevel) (UserProfileUpdateSummary 0 0 0 []) contacts idsEvts = map ctSndMsg changedCts
msgReqs_ <- zipWith ctMsgReq changedCts <$> createSndMessages idsEvts
(errs, cts) <- partitionEithers . zipWith (second . const) changedCts <$> deliverMessagesB msgReqs_
unless (null errs) $ toView $ CRChatErrors (Just user) errs
let changedCts' = filter (\ChangedProfileContact {ct, ct'} -> directOrUsed ct' && mergedPreferences ct' /= mergedPreferences ct) cts
createContactsSndFeatureItems user' changedCts'
let summary =
UserProfileUpdateSummary
{ updateSuccesses = length cts,
updateFailures = length errs,
changedContacts = map (\ChangedProfileContact {ct'} -> ct') changedCts'
}
pure $ CRUserProfileUpdated user' (fromLocalProfile p) p' summary pure $ CRUserProfileUpdated user' (fromLocalProfile p) p' summary
where where
processAndCount user' ll s@UserProfileUpdateSummary {notChanged, updateSuccesses, updateFailures, changedContacts = cts} ct = do -- [incognito] filter out contacts with whom user has incognito connections
let mergedProfile = userProfileToSend user Nothing $ Just ct addChangedProfileContact :: User -> Contact -> [ChangedProfileContact] -> [ChangedProfileContact]
ct' = updateMergedPreferences user' ct addChangedProfileContact user' ct changedCts = case contactSendConn_ ct' of
mergedProfile' = userProfileToSend user' Nothing $ Just ct' Left _ -> changedCts
if mergedProfile' == mergedProfile Right conn
then pure s {notChanged = notChanged + 1} | connIncognito conn || mergedProfile' == mergedProfile -> changedCts
else | otherwise -> ChangedProfileContact ct ct' mergedProfile' conn : changedCts
let cts' = if mergedPreferences ct == mergedPreferences ct' then cts else ct' : cts
in (notifyContact mergedProfile' ct' $> s {updateSuccesses = updateSuccesses + 1, changedContacts = cts'})
`catchChatError` \e -> when (ll <= CLLInfo) (toView $ CRChatError (Just user) e) $> s {updateFailures = updateFailures + 1, changedContacts = cts'}
where where
notifyContact mergedProfile' ct' = do mergedProfile = userProfileToSend user Nothing $ Just ct
void $ sendDirectContactMessage ct' (XInfo mergedProfile') ct' = updateMergedPreferences user' ct
when (directOrUsed ct') $ createSndFeatureItems user' ct ct' mergedProfile' = userProfileToSend user' Nothing $ Just ct'
ctSndMsg :: ChangedProfileContact -> (ConnOrGroupId, ChatMsgEvent 'Json)
ctSndMsg ChangedProfileContact {mergedProfile', conn = Connection {connId}} = (ConnectionId connId, XInfo mergedProfile')
ctMsgReq :: ChangedProfileContact -> Either ChatError SndMessage -> Either ChatError MsgReq
ctMsgReq ChangedProfileContact {conn} = fmap $ \SndMessage {msgId, msgBody} ->
(conn, MsgFlags {notification = hasNotification XInfo_}, msgBody, msgId)
updateContactPrefs :: User -> Contact -> Preferences -> m ChatResponse updateContactPrefs :: User -> Contact -> Preferences -> m ChatResponse
updateContactPrefs _ ct@Contact {activeConn = Nothing} _ = throwChatError $ CEContactNotActive ct updateContactPrefs _ ct@Contact {activeConn = Nothing} _ = throwChatError $ CEContactNotActive ct
updateContactPrefs user@User {userId} ct@Contact {activeConn = Just Connection {customUserProfileId}, userPreferences = contactUserPrefs} contactUserPrefs' updateContactPrefs user@User {userId} ct@Contact {activeConn = Just Connection {customUserProfileId}, userPreferences = contactUserPrefs} contactUserPrefs'
@@ -2399,6 +2416,13 @@ processChatCommand' vr = \case
cReqHashes = bimap hash hash cReqSchemas cReqHashes = bimap hash hash cReqSchemas
hash = ConnReqUriHash . C.sha256Hash . strEncode hash = ConnReqUriHash . C.sha256Hash . strEncode
data ChangedProfileContact = ChangedProfileContact
{ ct :: Contact,
ct' :: Contact,
mergedProfile' :: Profile,
conn :: Connection
}
prepareGroupMsg :: forall m. ChatMonad m => User -> GroupInfo -> MsgContent -> Maybe ChatItemId -> Maybe FileInvitation -> Maybe CITimed -> Bool -> m (MsgContainer, Maybe (CIQuote 'CTGroup)) 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 prepareGroupMsg user GroupInfo {groupId, membership} mc quotedItemId_ fInv_ timed_ live = case quotedItemId_ of
Nothing -> pure (MCSimple (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing) Nothing -> pure (MCSimple (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing)
@@ -2480,6 +2504,7 @@ startExpireCIThread user@User {userId} = do
flip catchChatError (toView . CRChatError (Just user)) $ do flip catchChatError (toView . CRChatError (Just user)) $ do
expireFlags <- asks expireCIFlags expireFlags <- asks expireCIFlags
atomically $ TM.lookup userId expireFlags >>= \b -> unless (b == Just True) retry atomically $ TM.lookup userId expireFlags >>= \b -> unless (b == Just True) retry
waitChatStartedAndActivated
ttl <- withStoreCtx' (Just "startExpireCIThread, getChatItemTTL") (`getChatItemTTL` user) ttl <- withStoreCtx' (Just "startExpireCIThread, getChatItemTTL") (`getChatItemTTL` user)
forM_ ttl $ \t -> expireChatItems user t False forM_ ttl $ \t -> expireChatItems user t False
liftIO $ threadDelay' interval liftIO $ threadDelay' interval
@@ -2973,7 +2998,7 @@ cleanupManager = do
stepDelay <- asks (cleanupManagerStepDelay . config) stepDelay <- asks (cleanupManagerStepDelay . config)
forever $ do forever $ do
flip catchChatError (toView . CRChatError Nothing) $ do flip catchChatError (toView . CRChatError Nothing) $ do
waitChatStarted waitChatStartedAndActivated
users <- withStoreCtx' (Just "cleanupManager, getUsers 1") getUsers users <- withStoreCtx' (Just "cleanupManager, getUsers 1") getUsers
let (us, us') = partition activeUser users let (us, us') = partition activeUser users
forM_ us $ cleanupUser interval stepDelay forM_ us $ cleanupUser interval stepDelay
@@ -2983,7 +3008,7 @@ cleanupManager = do
liftIO $ threadDelay' $ diffToMicroseconds interval liftIO $ threadDelay' $ diffToMicroseconds interval
where where
runWithoutInitialDelay cleanupInterval = flip catchChatError (toView . CRChatError Nothing) $ do runWithoutInitialDelay cleanupInterval = flip catchChatError (toView . CRChatError Nothing) $ do
waitChatStarted waitChatStartedAndActivated
users <- withStoreCtx' (Just "cleanupManager, getUsers 2") getUsers users <- withStoreCtx' (Just "cleanupManager, getUsers 2") getUsers
let (us, us') = partition activeUser users let (us, us') = partition activeUser users
forM_ us $ \u -> cleanupTimedItems cleanupInterval u `catchChatError` (toView . CRChatError (Just u)) forM_ us $ \u -> cleanupTimedItems cleanupInterval u `catchChatError` (toView . CRChatError (Just u))
@@ -3038,7 +3063,7 @@ deleteTimedItem :: ChatMonad m => User -> (ChatRef, ChatItemId) -> UTCTime -> m
deleteTimedItem user (ChatRef cType chatId, itemId) deleteAt = do deleteTimedItem user (ChatRef cType chatId, itemId) deleteAt = do
ts <- liftIO getCurrentTime ts <- liftIO getCurrentTime
liftIO $ threadDelay' $ diffToMicroseconds $ diffUTCTime deleteAt ts liftIO $ threadDelay' $ diffToMicroseconds $ diffUTCTime deleteAt ts
waitChatStarted waitChatStartedAndActivated
vr <- chatVersionRange vr <- chatVersionRange
case cType of case cType of
CTDirect -> do CTDirect -> do
@@ -3064,8 +3089,10 @@ expireChatItems user@User {userId} ttl sync = do
let expirationDate = addUTCTime (-1 * fromIntegral ttl) currentTs let expirationDate = addUTCTime (-1 * fromIntegral ttl) currentTs
-- this is to keep group messages created during last 12 hours even if they're expired according to item_ts -- this is to keep group messages created during last 12 hours even if they're expired according to item_ts
createdAtCutoff = addUTCTime (-43200 :: NominalDiffTime) currentTs createdAtCutoff = addUTCTime (-43200 :: NominalDiffTime) currentTs
waitChatStartedAndActivated
contacts <- withStoreCtx' (Just "expireChatItems, getUserContacts") (`getUserContacts` user) contacts <- withStoreCtx' (Just "expireChatItems, getUserContacts") (`getUserContacts` user)
loop contacts $ processContact expirationDate loop contacts $ processContact expirationDate
waitChatStartedAndActivated
groups <- withStoreCtx' (Just "expireChatItems, getUserGroupDetails") (\db -> getUserGroupDetails db vr user Nothing Nothing) groups <- withStoreCtx' (Just "expireChatItems, getUserGroupDetails") (\db -> getUserGroupDetails db vr user Nothing Nothing)
loop groups $ processGroup expirationDate createdAtCutoff loop groups $ processGroup expirationDate createdAtCutoff
where where
@@ -3084,11 +3111,13 @@ expireChatItems user@User {userId} ttl sync = do
when (expire == Just True) $ threadDelay 100000 >> a when (expire == Just True) $ threadDelay 100000 >> a
processContact :: UTCTime -> Contact -> m () processContact :: UTCTime -> Contact -> m ()
processContact expirationDate ct = do processContact expirationDate ct = do
waitChatStartedAndActivated
filesInfo <- withStoreCtx' (Just "processContact, getContactExpiredFileInfo") $ \db -> getContactExpiredFileInfo db user ct expirationDate filesInfo <- withStoreCtx' (Just "processContact, getContactExpiredFileInfo") $ \db -> getContactExpiredFileInfo db user ct expirationDate
deleteFilesAndConns user filesInfo deleteFilesAndConns user filesInfo
withStoreCtx' (Just "processContact, deleteContactExpiredCIs") $ \db -> deleteContactExpiredCIs db user ct expirationDate withStoreCtx' (Just "processContact, deleteContactExpiredCIs") $ \db -> deleteContactExpiredCIs db user ct expirationDate
processGroup :: UTCTime -> UTCTime -> GroupInfo -> m () processGroup :: UTCTime -> UTCTime -> GroupInfo -> m ()
processGroup expirationDate createdAtCutoff gInfo = do processGroup expirationDate createdAtCutoff gInfo = do
waitChatStartedAndActivated
filesInfo <- withStoreCtx' (Just "processGroup, getGroupExpiredFileInfo") $ \db -> getGroupExpiredFileInfo db user gInfo expirationDate createdAtCutoff filesInfo <- withStoreCtx' (Just "processGroup, getGroupExpiredFileInfo") $ \db -> getGroupExpiredFileInfo db user gInfo expirationDate createdAtCutoff
deleteFilesAndConns user filesInfo deleteFilesAndConns user filesInfo
withStoreCtx' (Just "processGroup, deleteGroupExpiredCIs") $ \db -> deleteGroupExpiredCIs db user gInfo expirationDate createdAtCutoff withStoreCtx' (Just "processGroup, deleteGroupExpiredCIs") $ \db -> deleteGroupExpiredCIs db user gInfo expirationDate createdAtCutoff
@@ -3356,6 +3385,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
sendXGrpMemInv hostConnId (Just directConnReq) xGrpMemIntroCont sendXGrpMemInv hostConnId (Just directConnReq) xGrpMemIntroCont
CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type" CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type"
MSG msgMeta _msgFlags msgBody -> do MSG msgMeta _msgFlags msgBody -> do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
cmdId <- createAckCmd conn cmdId <- createAckCmd conn
withAckMessage agentConnId cmdId msgMeta $ do withAckMessage agentConnId cmdId msgMeta $ do
(conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveDirectRcvMSG conn msgMeta cmdId msgBody (conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveDirectRcvMSG conn msgMeta cmdId msgBody
@@ -3364,14 +3394,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
updateChatLock "directMessage" event updateChatLock "directMessage" event
case event of case event of
XMsgNew mc -> newContentMessage ct' mc msg msgMeta XMsgNew mc -> newContentMessage ct' mc msg msgMeta
XMsgFileDescr sharedMsgId fileDescr -> messageFileDescription ct' sharedMsgId fileDescr msgMeta XMsgFileDescr sharedMsgId fileDescr -> messageFileDescription ct' sharedMsgId fileDescr
XMsgUpdate sharedMsgId mContent ttl live -> messageUpdate ct' sharedMsgId mContent msg msgMeta ttl live XMsgUpdate sharedMsgId mContent ttl live -> messageUpdate ct' sharedMsgId mContent msg msgMeta ttl live
XMsgDel sharedMsgId _ -> messageDelete ct' sharedMsgId msg msgMeta XMsgDel sharedMsgId _ -> messageDelete ct' sharedMsgId msg msgMeta
XMsgReact sharedMsgId _ reaction add -> directMsgReaction ct' sharedMsgId reaction add msg msgMeta XMsgReact sharedMsgId _ reaction add -> directMsgReaction ct' sharedMsgId reaction add msg msgMeta
-- TODO discontinue XFile -- TODO discontinue XFile
XFile fInv -> processFileInvitation' ct' fInv msg msgMeta XFile fInv -> processFileInvitation' ct' fInv msg msgMeta
XFileCancel sharedMsgId -> xFileCancel ct' sharedMsgId msgMeta XFileCancel sharedMsgId -> xFileCancel ct' sharedMsgId
XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct' sharedMsgId fileConnReq_ fName msgMeta XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct' sharedMsgId fileConnReq_ fName
XInfo p -> xInfo ct' p XInfo p -> xInfo ct' p
XDirectDel -> xDirectDel ct' msg msgMeta XDirectDel -> xDirectDel ct' msg msgMeta
XGrpInv gInv -> processGroupInvitation ct' gInv msg msgMeta XGrpInv gInv -> processGroupInvitation ct' gInv msg msgMeta
@@ -3379,10 +3409,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
XInfoProbeCheck probeHash -> xInfoProbeCheck (COMContact ct') probeHash XInfoProbeCheck probeHash -> xInfoProbeCheck (COMContact ct') probeHash
XInfoProbeOk probe -> xInfoProbeOk (COMContact ct') probe XInfoProbeOk probe -> xInfoProbeOk (COMContact ct') probe
XCallInv callId invitation -> xCallInv ct' callId invitation msg msgMeta XCallInv callId invitation -> xCallInv ct' callId invitation msg msgMeta
XCallOffer callId offer -> xCallOffer ct' callId offer msg msgMeta XCallOffer callId offer -> xCallOffer ct' callId offer msg
XCallAnswer callId answer -> xCallAnswer ct' callId answer msg msgMeta XCallAnswer callId answer -> xCallAnswer ct' callId answer msg
XCallExtra callId extraInfo -> xCallExtra ct' callId extraInfo msg msgMeta XCallExtra callId extraInfo -> xCallExtra ct' callId extraInfo msg
XCallEnd callId -> xCallEnd ct' callId msg msgMeta XCallEnd callId -> xCallEnd ct' callId msg
BFileChunk sharedMsgId chunk -> bFileChunk ct' sharedMsgId chunk msgMeta BFileChunk sharedMsgId chunk -> bFileChunk ct' sharedMsgId chunk msgMeta
_ -> messageError $ "unsupported message: " <> T.pack (show event) _ -> messageError $ "unsupported message: " <> T.pack (show event)
let Contact {chatSettings = ChatSettings {sendRcpts}} = ct' let Contact {chatSettings = ChatSettings {sendRcpts}} = ct'
@@ -3740,7 +3770,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
void $ sendDirectMessage imConn (XGrpMemCon memberId) (GroupId groupId) void $ sendDirectMessage imConn (XGrpMemCon memberId) (GroupId groupId)
_ -> messageWarning "sendXGrpMemCon: member category GCPreMember or GCPostMember is expected" _ -> messageWarning "sendXGrpMemCon: member category GCPreMember or GCPostMember is expected"
MSG msgMeta _msgFlags msgBody -> do MSG msgMeta _msgFlags msgBody -> do
checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure () checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta
cmdId <- createAckCmd conn cmdId <- createAckCmd conn
let aChatMsgs = parseChatMessages msgBody let aChatMsgs = parseChatMessages msgBody
withAckMessage agentConnId cmdId msgMeta $ do withAckMessage agentConnId cmdId msgMeta $ do
@@ -4231,7 +4261,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
newContentMessage :: Contact -> MsgContainer -> RcvMessage -> MsgMeta -> m () newContentMessage :: Contact -> MsgContainer -> RcvMessage -> MsgMeta -> m ()
newContentMessage ct@Contact {contactUsed} mc msg@RcvMessage {sharedMsgId_} msgMeta = do newContentMessage ct@Contact {contactUsed} mc msg@RcvMessage {sharedMsgId_} msgMeta = do
unless contactUsed $ withStore' $ \db -> updateContactUsed db user ct unless contactUsed $ withStore' $ \db -> updateContactUsed db user ct
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
let ExtMsgContent content fInv_ _ _ = mcExtMsgContent mc let ExtMsgContent content fInv_ _ _ = mcExtMsgContent mc
-- Uncomment to test stuck delivery on errors - see test testDirectMessageDelete -- Uncomment to test stuck delivery on errors - see test testDirectMessageDelete
-- case content of -- case content of
@@ -4261,9 +4290,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
ChatConfig {autoAcceptFileSize = sz} <- asks config ChatConfig {autoAcceptFileSize = sz} <- asks config
when (sz > fileSize) $ receiveFile' user ft Nothing Nothing >>= toView when (sz > fileSize) $ receiveFile' user ft Nothing Nothing >>= toView
messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> MsgMeta -> m () messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> m ()
messageFileDescription ct@Contact {contactId} sharedMsgId fileDescr msgMeta = do messageFileDescription Contact {contactId} sharedMsgId fileDescr = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId
processFDMessage fileId fileDescr processFDMessage fileId fileDescr
@@ -4306,7 +4334,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> Maybe Int -> Maybe Bool -> m () messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> Maybe Int -> Maybe Bool -> m ()
messageUpdate ct@Contact {contactId} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl live_ = do messageUpdate ct@Contact {contactId} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl live_ = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
updateRcvChatItem `catchCINotFound` \_ -> do updateRcvChatItem `catchCINotFound` \_ -> do
-- This patches initial sharedMsgId into chat item when locally deleted chat item -- This patches initial sharedMsgId into chat item when locally deleted chat item
-- received an update from the sender, so that it can be referenced later (e.g. by broadcast delete). -- received an update from the sender, so that it can be referenced later (e.g. by broadcast delete).
@@ -4339,10 +4366,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
_ -> messageError "x.msg.update: contact attempted invalid message update" _ -> messageError "x.msg.update: contact attempted invalid message update"
messageDelete :: Contact -> SharedMsgId -> RcvMessage -> MsgMeta -> m () messageDelete :: Contact -> SharedMsgId -> RcvMessage -> MsgMeta -> m ()
messageDelete ct@Contact {contactId} sharedMsgId RcvMessage {msgId} msgMeta@MsgMeta {broker = (_, brokerTs)} = do messageDelete ct@Contact {contactId} sharedMsgId RcvMessage {msgId} msgMeta = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
deleteRcvChatItem `catchCINotFound` (toView . CRChatItemDeletedNotFound user ct) deleteRcvChatItem `catchCINotFound` (toView . CRChatItemDeletedNotFound user ct)
where where
brokerTs = metaBrokerTs msgMeta
deleteRcvChatItem = do deleteRcvChatItem = do
CChatItem msgDir ci <- withStore $ \db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId CChatItem msgDir ci <- withStore $ \db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId
case msgDir of case msgDir of
@@ -4510,7 +4537,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
-- TODO remove once XFile is discontinued -- TODO remove once XFile is discontinued
processFileInvitation' :: Contact -> FileInvitation -> RcvMessage -> MsgMeta -> m () processFileInvitation' :: Contact -> FileInvitation -> RcvMessage -> MsgMeta -> m ()
processFileInvitation' ct fInv@FileInvitation {fileName, fileSize} msg@RcvMessage {sharedMsgId_} msgMeta = do processFileInvitation' ct fInv@FileInvitation {fileName, fileSize} msg@RcvMessage {sharedMsgId_} msgMeta = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
ChatConfig {fileChunkSize} <- asks config ChatConfig {fileChunkSize} <- asks config
inline <- receiveInlineMode fInv Nothing fileChunkSize inline <- receiveInlineMode fInv Nothing fileChunkSize
RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFileTransfer db userId ct fInv inline fileChunkSize RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFileTransfer db userId ct fInv inline fileChunkSize
@@ -4547,9 +4573,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
inline' receiveInstant = if mode == IFMOffer || (receiveInstant && maybe False isVoice mc_) then fileInline else Nothing inline' receiveInstant = if mode == IFMOffer || (receiveInstant && maybe False isVoice mc_) then fileInline else Nothing
_ -> pure Nothing _ -> pure Nothing
xFileCancel :: Contact -> SharedMsgId -> MsgMeta -> m () xFileCancel :: Contact -> SharedMsgId -> m ()
xFileCancel ct@Contact {contactId} sharedMsgId msgMeta = do xFileCancel Contact {contactId} sharedMsgId = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId
ft <- withStore (\db -> getRcvFileTransfer db user fileId) ft <- withStore (\db -> getRcvFileTransfer db user fileId)
unless (rcvFileCompleteOrCancelled ft) $ do unless (rcvFileCompleteOrCancelled ft) $ do
@@ -4557,9 +4582,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
ci <- withStore $ \db -> getChatItemByFileId db vr user fileId ci <- withStore $ \db -> getChatItemByFileId db vr user fileId
toView $ CRRcvFileSndCancelled user ci ft toView $ CRRcvFileSndCancelled user ci ft
xFileAcptInv :: Contact -> SharedMsgId -> Maybe ConnReqInvitation -> String -> MsgMeta -> m () xFileAcptInv :: Contact -> SharedMsgId -> Maybe ConnReqInvitation -> String -> m ()
xFileAcptInv ct sharedMsgId fileConnReq_ fName msgMeta = do xFileAcptInv ct sharedMsgId fileConnReq_ fName = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
fileId <- withStore $ \db -> getDirectFileIdBySharedMsgId db user ct sharedMsgId fileId <- withStore $ \db -> getDirectFileIdBySharedMsgId db user ct sharedMsgId
(AChatItem _ _ _ ci) <- withStore $ \db -> getChatItemByFileId db vr user fileId (AChatItem _ _ _ ci) <- withStore $ \db -> getChatItemByFileId db vr user fileId
assertSMPAcceptNotProhibited ci assertSMPAcceptNotProhibited ci
@@ -4693,7 +4717,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
let Contact {localDisplayName = c, activeConn} = ct let Contact {localDisplayName = c, activeConn} = ct
GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole), connRequest, groupLinkId} = inv GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole), connRequest, groupLinkId} = inv
forM_ activeConn $ \Connection {connId, peerChatVRange, customUserProfileId, groupLinkId = groupLinkId'} -> do forM_ activeConn $ \Connection {connId, peerChatVRange, customUserProfileId, groupLinkId = groupLinkId'} -> do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
when (fromRole < GRAdmin || fromRole < memRole) $ throwChatError (CEGroupContactRole c) when (fromRole < GRAdmin || fromRole < memRole) $ throwChatError (CEGroupContactRole c)
when (fromMemId == memId) $ throwChatError CEGroupDuplicateMemberId when (fromMemId == memId) $ throwChatError CEGroupDuplicateMemberId
-- [incognito] if direct connection with host is incognito, create membership using the same incognito profile -- [incognito] if direct connection with host is incognito, create membership using the same incognito profile
@@ -4725,7 +4748,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
checkIntegrityCreateItem :: forall c. ChatTypeI c => ChatDirection c 'MDRcv -> MsgMeta -> m () checkIntegrityCreateItem :: forall c. ChatTypeI c => ChatDirection c 'MDRcv -> MsgMeta -> m ()
checkIntegrityCreateItem cd MsgMeta {integrity, broker = (_, brokerTs)} = case integrity of checkIntegrityCreateItem cd MsgMeta {integrity, broker = (_, brokerTs)} = case integrity of
MsgOk -> pure () MsgOk -> pure ()
MsgError e -> createInternalChatItem user cd (CIRcvIntegrityError e) (Just brokerTs) MsgError e ->
createInternalChatItem user cd (CIRcvIntegrityError e) (Just brokerTs)
`catchChatError` \_ -> pure ()
xInfo :: Contact -> Profile -> m () xInfo :: Contact -> Profile -> m ()
xInfo c p' = void $ processContactProfileUpdate c p' True xInfo c p' = void $ processContactProfileUpdate c p' True
@@ -4734,7 +4759,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
xDirectDel c msg msgMeta = xDirectDel c msg msgMeta =
if directOrUsed c if directOrUsed c
then do then do
checkIntegrityCreateItem (CDDirectRcv c) msgMeta
ct' <- withStore' $ \db -> updateContactStatus db user c CSDeleted ct' <- withStore' $ \db -> updateContactStatus db user c CSDeleted
contactConns <- withStore' $ \db -> getContactConnections db userId ct' contactConns <- withStore' $ \db -> getContactConnections db userId ct'
deleteAgentConnectionsAsync user $ map aConnId contactConns deleteAgentConnectionsAsync user $ map aConnId contactConns
@@ -4894,7 +4918,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
-- to party accepting call -- to party accepting call
xCallInv :: Contact -> CallId -> CallInvitation -> RcvMessage -> MsgMeta -> m () xCallInv :: Contact -> CallId -> CallInvitation -> RcvMessage -> MsgMeta -> m ()
xCallInv ct@Contact {contactId} callId CallInvitation {callType, callDhPubKey} msg@RcvMessage {sharedMsgId_} msgMeta = do xCallInv ct@Contact {contactId} callId CallInvitation {callType, callDhPubKey} msg@RcvMessage {sharedMsgId_} msgMeta = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
if featureAllowed SCFCalls forContact ct if featureAllowed SCFCalls forContact ct
then do then do
g <- asks random g <- asks random
@@ -4921,9 +4944,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci)
-- to party initiating call -- to party initiating call
xCallOffer :: Contact -> CallId -> CallOffer -> RcvMessage -> MsgMeta -> m () xCallOffer :: Contact -> CallId -> CallOffer -> RcvMessage -> m ()
xCallOffer ct callId CallOffer {callType, rtcSession, callDhPubKey} msg msgMeta = do xCallOffer ct callId CallOffer {callType, rtcSession, callDhPubKey} msg = do
msgCurrentCall ct callId "x.call.offer" msg msgMeta $ msgCurrentCall ct callId "x.call.offer" msg $
\call -> case callState call of \call -> case callState call of
CallInvitationSent {localCallType, localDhPrivKey} -> do CallInvitationSent {localCallType, localDhPrivKey} -> do
let sharedKey = C.Key . C.dhBytes' <$> (C.dh' <$> callDhPubKey <*> localDhPrivKey) let sharedKey = C.Key . C.dhBytes' <$> (C.dh' <$> callDhPubKey <*> localDhPrivKey)
@@ -4936,9 +4959,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
pure (Just call, Nothing) pure (Just call, Nothing)
-- to party accepting call -- to party accepting call
xCallAnswer :: Contact -> CallId -> CallAnswer -> RcvMessage -> MsgMeta -> m () xCallAnswer :: Contact -> CallId -> CallAnswer -> RcvMessage -> m ()
xCallAnswer ct callId CallAnswer {rtcSession} msg msgMeta = do xCallAnswer ct callId CallAnswer {rtcSession} msg = do
msgCurrentCall ct callId "x.call.answer" msg msgMeta $ msgCurrentCall ct callId "x.call.answer" msg $
\call -> case callState call of \call -> case callState call of
CallOfferSent {localCallType, peerCallType, localCallSession, sharedKey} -> do CallOfferSent {localCallType, peerCallType, localCallSession, sharedKey} -> do
let callState' = CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession = rtcSession, sharedKey} let callState' = CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession = rtcSession, sharedKey}
@@ -4949,9 +4972,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
pure (Just call, Nothing) pure (Just call, Nothing)
-- to any call party -- to any call party
xCallExtra :: Contact -> CallId -> CallExtraInfo -> RcvMessage -> MsgMeta -> m () xCallExtra :: Contact -> CallId -> CallExtraInfo -> RcvMessage -> m ()
xCallExtra ct callId CallExtraInfo {rtcExtraInfo} msg msgMeta = do xCallExtra ct callId CallExtraInfo {rtcExtraInfo} msg = do
msgCurrentCall ct callId "x.call.extra" msg msgMeta $ msgCurrentCall ct callId "x.call.extra" msg $
\call -> case callState call of \call -> case callState call of
CallOfferReceived {localCallType, peerCallType, peerCallSession, sharedKey} -> do CallOfferReceived {localCallType, peerCallType, peerCallSession, sharedKey} -> do
-- TODO update the list of ice servers in peerCallSession -- TODO update the list of ice servers in peerCallSession
@@ -4968,15 +4991,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
pure (Just call, Nothing) pure (Just call, Nothing)
-- to any call party -- to any call party
xCallEnd :: Contact -> CallId -> RcvMessage -> MsgMeta -> m () xCallEnd :: Contact -> CallId -> RcvMessage -> m ()
xCallEnd ct callId msg msgMeta = xCallEnd ct callId msg =
msgCurrentCall ct callId "x.call.end" msg msgMeta $ \Call {chatItemId} -> do msgCurrentCall ct callId "x.call.end" msg $ \Call {chatItemId} -> do
toView $ CRCallEnded user ct toView $ CRCallEnded user ct
(Nothing,) <$> callStatusItemContent user ct chatItemId WCSDisconnected (Nothing,) <$> callStatusItemContent user ct chatItemId WCSDisconnected
msgCurrentCall :: Contact -> CallId -> Text -> RcvMessage -> MsgMeta -> (Call -> m (Maybe Call, Maybe ACIContent)) -> m () msgCurrentCall :: Contact -> CallId -> Text -> RcvMessage -> (Call -> m (Maybe Call, Maybe ACIContent)) -> m ()
msgCurrentCall ct@Contact {contactId = ctId'} callId' eventName RcvMessage {msgId} msgMeta action = do msgCurrentCall ct@Contact {contactId = ctId'} callId' eventName RcvMessage {msgId} action = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
calls <- asks currentCalls calls <- asks currentCalls
atomically (TM.lookup ctId' calls) >>= \case atomically (TM.lookup ctId' calls) >>= \case
Nothing -> messageError $ eventName <> ": no current call" Nothing -> messageError $ eventName <> ": no current call"
@@ -5631,12 +5653,20 @@ deleteOrUpdateMemberRecord user@User {userId} member =
Nothing -> deleteGroupMember db user member Nothing -> deleteGroupMember db user member
sendDirectContactMessage :: (MsgEncodingI e, ChatMonad m) => Contact -> ChatMsgEvent e -> m (SndMessage, Int64) sendDirectContactMessage :: (MsgEncodingI e, ChatMonad m) => Contact -> ChatMsgEvent e -> m (SndMessage, Int64)
sendDirectContactMessage ct@Contact {activeConn = Nothing} _ = throwChatError $ CEContactNotReady ct sendDirectContactMessage ct chatMsgEvent = do
sendDirectContactMessage ct@Contact {activeConn = Just conn@Connection {connId, connStatus}, contactStatus} chatMsgEvent conn@Connection {connId} <- liftEither $ contactSendConn_ ct
| connStatus /= ConnReady && connStatus /= ConnSndReady = throwChatError $ CEContactNotReady ct sendDirectMessage conn chatMsgEvent (ConnectionId connId)
| contactStatus /= CSActive = throwChatError $ CEContactNotActive ct
| connDisabled conn = throwChatError $ CEContactDisabled ct contactSendConn_ :: Contact -> Either ChatError Connection
| otherwise = sendDirectMessage conn chatMsgEvent (ConnectionId connId) contactSendConn_ ct@Contact {activeConn} = case activeConn of
Nothing -> err $ CEContactNotReady ct
Just conn
| not (connReady conn) -> err $ CEContactNotReady ct
| not (contactActive ct) -> err $ CEContactNotActive ct
| connDisabled conn -> err $ CEContactDisabled ct
| otherwise -> Right conn
where
err = Left . ChatError
sendDirectMessage :: (MsgEncodingI e, ChatMonad m) => Connection -> ChatMsgEvent e -> ConnOrGroupId -> m (SndMessage, Int64) sendDirectMessage :: (MsgEncodingI e, ChatMonad m) => Connection -> ChatMsgEvent e -> ConnOrGroupId -> m (SndMessage, Int64)
sendDirectMessage conn chatMsgEvent connOrGroupId = do sendDirectMessage conn chatMsgEvent connOrGroupId = do
@@ -5645,18 +5675,25 @@ sendDirectMessage conn chatMsgEvent connOrGroupId = do
(msg,) <$> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId (msg,) <$> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId
createSndMessage :: (MsgEncodingI e, ChatMonad m) => ChatMsgEvent e -> ConnOrGroupId -> m SndMessage createSndMessage :: (MsgEncodingI e, ChatMonad m) => ChatMsgEvent e -> ConnOrGroupId -> m SndMessage
createSndMessage chatMsgEvent connOrGroupId = do createSndMessage chatMsgEvent connOrGroupId =
liftEither . runIdentity =<< createSndMessages (Identity (connOrGroupId, chatMsgEvent))
createSndMessages :: forall e m t. (MsgEncodingI e, ChatMonad' m, Traversable t) => t (ConnOrGroupId, ChatMsgEvent e) -> m (t (Either ChatError SndMessage))
createSndMessages idsEvents = do
gVar <- asks random gVar <- asks random
vr <- chatVersionRange vr <- chatVersionRange
withStore $ \db -> createNewSndMessage db gVar connOrGroupId chatMsgEvent (encodeMessage vr) withStoreBatch $ \db -> fmap (uncurry (createMsg db gVar vr)) idsEvents
where where
encodeMessage chatVRange sharedMsgId = createMsg db gVar chatVRange connOrGroupId evnt = runExceptT $ do
encodeChatMessage ChatMessage {chatVRange, msgId = Just sharedMsgId, chatMsgEvent} withExceptT ChatErrorStore $ createNewSndMessage db gVar connOrGroupId evnt (encodeMessage chatVRange evnt)
encodeMessage chatVRange evnt sharedMsgId =
encodeChatMessage ChatMessage {chatVRange, msgId = Just sharedMsgId, chatMsgEvent = evnt}
sendGroupMemberMessages :: forall e m. (MsgEncodingI e, ChatMonad m) => User -> Connection -> NonEmpty (ChatMsgEvent e) -> GroupId -> m () sendGroupMemberMessages :: forall e m. (MsgEncodingI e, ChatMonad m) => User -> Connection -> NonEmpty (ChatMsgEvent e) -> GroupId -> m ()
sendGroupMemberMessages user conn@Connection {connId} events groupId = do sendGroupMemberMessages user conn@Connection {connId} events groupId = do
when (connDisabled conn) $ throwChatError (CEConnectionDisabled conn) when (connDisabled conn) $ throwChatError (CEConnectionDisabled conn)
(errs, msgs) <- partitionEithers <$> createSndMessages let idsEvts = L.map (GroupId groupId,) events
(errs, msgs) <- partitionEithers . L.toList <$> createSndMessages idsEvts
unless (null errs) $ toView $ CRChatErrors (Just user) errs unless (null errs) $ toView $ CRChatErrors (Just user) errs
unless (null msgs) $ do unless (null msgs) $ do
let (errs', msgBatches) = partitionEithers $ batchMessages maxChatMsgSize msgs let (errs', msgBatches) = partitionEithers $ batchMessages maxChatMsgSize msgs
@@ -5671,16 +5708,6 @@ sendGroupMemberMessages user conn@Connection {connId} events groupId = do
agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) MsgFlags {notification = True} batchBody agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) MsgFlags {notification = True} batchBody
let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId} let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId}
void . withStoreBatch' $ \db -> map (\SndMessage {msgId} -> createSndMsgDelivery db sndMsgDelivery msgId) sndMsgs void . withStoreBatch' $ \db -> map (\SndMessage {msgId} -> createSndMsgDelivery db sndMsgDelivery msgId) sndMsgs
createSndMessages :: m [Either ChatError SndMessage]
createSndMessages = do
gVar <- asks random
vr <- chatVersionRange
withStoreBatch $ \db -> map (createMsg db gVar vr) (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 :: (MsgEncodingI e, ChatMonad m) => ChatMsgEvent e -> m ByteString
directMessage chatMsgEvent = do directMessage chatMsgEvent = do
@@ -5701,14 +5728,23 @@ deliverMessage' conn msgFlags msgBody msgId =
[r] -> liftEither r [r] -> liftEither r
rs -> throwChatError $ CEInternalError $ "deliverMessage: expected 1 result, got " <> show (length rs) rs -> throwChatError $ CEInternalError $ "deliverMessage: expected 1 result, got " <> show (length rs)
deliverMessages :: ChatMonad' m => [(Connection, MsgFlags, LazyMsgBody, MessageId)] -> m [Either ChatError Int64] type MsgReq = (Connection, MsgFlags, LazyMsgBody, MessageId)
deliverMessages msgReqs = do
sent <- zipWith prepareBatch msgReqs <$> withAgent' (`sendMessages` aReqs) deliverMessages :: ChatMonad' m => [MsgReq] -> m [Either ChatError Int64]
deliverMessages = deliverMessagesB . map Right
deliverMessagesB :: ChatMonad' m => [Either ChatError MsgReq] -> m [Either ChatError Int64]
deliverMessagesB msgReqs = do
sent <- zipWith prepareBatch msgReqs <$> withAgent' (`sendMessagesB` map toAgent msgReqs)
withStoreBatch $ \db -> map (bindRight $ createDelivery db) sent withStoreBatch $ \db -> map (bindRight $ createDelivery db) sent
where where
aReqs = map (\(conn, msgFlags, msgBody, _msgId) -> (aConnId conn, msgFlags, LB.toStrict msgBody)) msgReqs toAgent = \case
prepareBatch req = bimap (`ChatErrorAgent` Nothing) (req,) Right (conn, msgFlags, msgBody, _msgId) -> Right (aConnId conn, msgFlags, LB.toStrict msgBody)
createDelivery :: DB.Connection -> ((Connection, MsgFlags, LazyMsgBody, MessageId), AgentMsgId) -> IO (Either ChatError Int64) Left _ce -> Left (AP.INTERNAL "ChatError, skip") -- as long as it is Left, the agent batchers should just step over it
prepareBatch (Right req) (Right ar) = Right (req, ar)
prepareBatch (Left ce) _ = Left ce -- restore original ChatError
prepareBatch _ (Left ae) = Left $ ChatErrorAgent ae Nothing
createDelivery :: DB.Connection -> (MsgReq, AgentMsgId) -> IO (Either ChatError Int64)
createDelivery db ((Connection {connId}, _, _, msgId), agentMsgId) = createDelivery db ((Connection {connId}, _, _, msgId), agentMsgId) =
Right <$> createSndMsgDelivery db (SndMsgDelivery {connId, agentMsgId}) msgId Right <$> createSndMsgDelivery db (SndMsgDelivery {connId, agentMsgId}) msgId
@@ -5854,7 +5890,7 @@ saveSndChatItem' user cd msg@SndMessage {sharedMsgId} content ciFile quotedItem
ciId <- createNewSndChatItem db user cd msg content quotedItem itemTimed live createdAt ciId <- createNewSndChatItem db user cd msg content quotedItem itemTimed live createdAt
forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt
pure ciId pure ciId
liftIO $ mkChatItem cd ciId content ciFile quotedItem (Just sharedMsgId) itemTimed live createdAt Nothing createdAt pure $ mkChatItem cd ciId content ciFile quotedItem (Just sharedMsgId) itemTimed live createdAt Nothing createdAt
saveRcvChatItem :: ChatMonad m => User -> ChatDirection c 'MDRcv -> RcvMessage -> UTCTime -> CIContent 'MDRcv -> m (ChatItem c 'MDRcv) saveRcvChatItem :: ChatMonad m => User -> ChatDirection c 'MDRcv -> RcvMessage -> UTCTime -> CIContent 'MDRcv -> m (ChatItem c 'MDRcv)
saveRcvChatItem user cd msg@RcvMessage {sharedMsgId_} brokerTs content = saveRcvChatItem user cd msg@RcvMessage {sharedMsgId_} brokerTs content =
@@ -5868,14 +5904,14 @@ saveRcvChatItem' user cd msg@RcvMessage {forwardedByMember} sharedMsgId_ brokerT
(ciId, quotedItem) <- createNewRcvChatItem db user cd msg sharedMsgId_ content itemTimed live brokerTs createdAt (ciId, quotedItem) <- createNewRcvChatItem db user cd msg sharedMsgId_ content itemTimed live brokerTs createdAt
forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt
pure (ciId, quotedItem) pure (ciId, quotedItem)
liftIO $ mkChatItem cd ciId content ciFile quotedItem sharedMsgId_ itemTimed live brokerTs forwardedByMember createdAt pure $ mkChatItem cd ciId content ciFile quotedItem sharedMsgId_ itemTimed live brokerTs forwardedByMember createdAt
mkChatItem :: forall c d. MsgDirectionI d => ChatDirection c d -> ChatItemId -> CIContent d -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CITimed -> Bool -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> IO (ChatItem c d) mkChatItem :: forall c d. MsgDirectionI d => ChatDirection c d -> ChatItemId -> CIContent d -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CITimed -> Bool -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> ChatItem c d
mkChatItem cd ciId content file quotedItem sharedMsgId itemTimed live itemTs forwardedByMember currentTs = do mkChatItem cd ciId content file quotedItem sharedMsgId itemTimed live itemTs forwardedByMember currentTs =
let itemText = ciContentToText content let itemText = ciContentToText content
itemStatus = ciCreateStatus content itemStatus = ciCreateStatus content
meta = mkCIMeta ciId content itemText itemStatus sharedMsgId Nothing False itemTimed (justTrue live) currentTs itemTs forwardedByMember currentTs currentTs meta = mkCIMeta ciId content itemText itemStatus sharedMsgId Nothing False itemTimed (justTrue live) currentTs itemTs forwardedByMember currentTs currentTs
pure ChatItem {chatDir = toCIDirection cd, meta, content, formattedText = parseMaybeMarkdownList itemText, quotedItem, reactions = [], file} in ChatItem {chatDir = toCIDirection cd, meta, content, formattedText = parseMaybeMarkdownList itemText, quotedItem, reactions = [], file}
deleteDirectCI :: (ChatMonad m, MsgDirectionI d) => User -> Contact -> ChatItem 'CTDirect d -> Bool -> Bool -> m ChatResponse deleteDirectCI :: (ChatMonad m, MsgDirectionI d) => User -> Contact -> ChatItem 'CTDirect d -> Bool -> Bool -> m ChatResponse
deleteDirectCI user ct ci@ChatItem {file} byUser timed = do deleteDirectCI user ct ci@ChatItem {file} byUser timed = do
@@ -5988,6 +6024,15 @@ createSndFeatureItems user ct ct' =
CUPContact {preference} -> preference CUPContact {preference} -> preference
CUPUser {preference} -> preference CUPUser {preference} -> preference
createContactsSndFeatureItems :: forall m. ChatMonad m => User -> [ChangedProfileContact] -> m ()
createContactsSndFeatureItems user cts =
createContactsFeatureItems user cts' CDDirectSnd CISndChatFeature CISndChatPreference getPref
where
cts' = map (\ChangedProfileContact {ct, ct'} -> (ct, ct')) cts
getPref ContactUserPreference {userPreference} = case userPreference of
CUPContact {preference} -> preference
CUPUser {preference} -> preference
type FeatureContent a d = ChatFeature -> a -> Maybe Int -> CIContent d type FeatureContent a d = ChatFeature -> a -> Maybe Int -> CIContent d
createFeatureItems :: createFeatureItems ::
@@ -6001,24 +6046,44 @@ createFeatureItems ::
FeatureContent FeatureAllowed d -> FeatureContent FeatureAllowed d ->
(forall f. ContactUserPreference (FeaturePreference f) -> FeaturePreference f) -> (forall f. ContactUserPreference (FeaturePreference f) -> FeaturePreference f) ->
m () m ()
createFeatureItems user Contact {mergedPreferences = cups} ct'@Contact {mergedPreferences = cups'} chatDir ciFeature ciOffer getPref = createFeatureItems user ct ct' = createContactsFeatureItems user [(ct, ct')]
forM_ allChatFeatures $ \(ACF f) -> createItem f
createContactsFeatureItems ::
forall d m.
(MsgDirectionI d, ChatMonad m) =>
User ->
[(Contact, Contact)] ->
(Contact -> ChatDirection 'CTDirect d) ->
FeatureContent PrefEnabled d ->
FeatureContent FeatureAllowed d ->
(forall f. ContactUserPreference (FeaturePreference f) -> FeaturePreference f) ->
m ()
createContactsFeatureItems user cts chatDir ciFeature ciOffer getPref = do
let dirsCIContents = map contactChangedFeatures cts
(errs, acis) <- partitionEithers <$> createInternalItemsForChats user Nothing dirsCIContents
unless (null errs) $ toView $ CRChatErrors (Just user) errs
forM_ acis $ \aci -> toView $ CRNewChatItem user aci
where where
createItem :: forall f. FeatureI f => SChatFeature f -> m () contactChangedFeatures :: (Contact, Contact) -> (ChatDirection 'CTDirect d, [CIContent d])
createItem f contactChangedFeatures (Contact {mergedPreferences = cups}, ct'@Contact {mergedPreferences = cups'}) = do
| state /= state' = create ciFeature state' let contents = mapMaybe (\(ACF f) -> featureCIContent_ f) allChatFeatures
| prefState /= prefState' = create ciOffer prefState' (chatDir ct', contents)
| otherwise = pure ()
where where
create :: FeatureContent a d -> (a, Maybe Int) -> m () featureCIContent_ :: forall f. FeatureI f => SChatFeature f -> Maybe (CIContent d)
create ci (s, param) = createInternalChatItem user (chatDir ct') (ci f' s param) Nothing featureCIContent_ f
f' = chatFeature f | state /= state' = Just $ fContent ciFeature state'
state = featureState cup | prefState /= prefState' = Just $ fContent ciOffer prefState'
state' = featureState cup' | otherwise = Nothing
prefState = preferenceState $ getPref cup where
prefState' = preferenceState $ getPref cup' fContent :: FeatureContent a d -> (a, Maybe Int) -> CIContent d
cup = getContactUserPreference f cups fContent ci (s, param) = ci f' s param
cup' = getContactUserPreference f cups' f' = chatFeature f
state = featureState cup
state' = featureState cup'
prefState = preferenceState $ getPref cup
prefState' = preferenceState $ getPref cup'
cup = getContactUserPreference f cups
cup' = getContactUserPreference f cups'
createGroupFeatureChangedItems :: (MsgDirectionI d, ChatMonad m) => User -> ChatDirection 'CTGroup d -> (GroupFeature -> GroupPreference -> Maybe Int -> CIContent d) -> GroupInfo -> GroupInfo -> m () createGroupFeatureChangedItems :: (MsgDirectionI d, ChatMonad m) => User -> ChatDirection 'CTGroup d -> (GroupFeature -> GroupPreference -> Maybe Int -> CIContent d) -> GroupInfo -> GroupInfo -> m ()
createGroupFeatureChangedItems user cd ciContent GroupInfo {fullGroupPreferences = gps} GroupInfo {fullGroupPreferences = gps'} = createGroupFeatureChangedItems user cd ciContent GroupInfo {fullGroupPreferences = gps} GroupInfo {fullGroupPreferences = gps'} =
@@ -6032,15 +6097,35 @@ createGroupFeatureChangedItems user cd ciContent GroupInfo {fullGroupPreferences
sameGroupProfileInfo :: GroupProfile -> GroupProfile -> Bool sameGroupProfileInfo :: GroupProfile -> GroupProfile -> Bool
sameGroupProfileInfo p p' = p {groupPreferences = Nothing} == p' {groupPreferences = Nothing} sameGroupProfileInfo p p' = p {groupPreferences = Nothing} == p' {groupPreferences = Nothing}
createInternalChatItem :: forall c d m. (ChatTypeI c, MsgDirectionI d, ChatMonad m) => User -> ChatDirection c d -> CIContent d -> Maybe UTCTime -> m () createInternalChatItem :: (ChatTypeI c, MsgDirectionI d, ChatMonad m) => User -> ChatDirection c d -> CIContent d -> Maybe UTCTime -> m ()
createInternalChatItem user cd content itemTs_ = do createInternalChatItem user cd content itemTs_ =
createInternalItemsForChats user itemTs_ [(cd, [content])] >>= \case
[Right aci] -> toView $ CRNewChatItem user aci
[Left e] -> throwError e
rs -> throwChatError $ CEInternalError $ "createInternalChatItem: expected 1 result, got " <> show (length rs)
createInternalItemsForChats ::
forall c d m.
(ChatTypeI c, MsgDirectionI d, ChatMonad' m) =>
User ->
Maybe UTCTime ->
[(ChatDirection c d, [CIContent d])] ->
m [Either ChatError AChatItem]
createInternalItemsForChats user itemTs_ dirsCIContents = do
createdAt <- liftIO getCurrentTime createdAt <- liftIO getCurrentTime
let itemTs = fromMaybe createdAt itemTs_ let itemTs = fromMaybe createdAt itemTs_
ciId <- withStore' $ \db -> do void . withStoreBatch' $ \db -> map (uncurry $ updateChat db createdAt) dirsCIContents
when (ciRequiresAttention content) $ updateChatTs db user cd createdAt withStoreBatch' $ \db -> concatMap (uncurry $ createACIs db itemTs createdAt) dirsCIContents
createNewChatItemNoMsg db user cd content itemTs createdAt where
ci <- liftIO $ mkChatItem cd ciId content Nothing Nothing Nothing Nothing False itemTs Nothing createdAt updateChat :: DB.Connection -> UTCTime -> ChatDirection c d -> [CIContent d] -> IO ()
toView $ CRNewChatItem user (AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci) updateChat db createdAt cd contents
| any ciRequiresAttention contents = updateChatTs db user cd createdAt
| otherwise = pure ()
createACIs :: DB.Connection -> UTCTime -> UTCTime -> ChatDirection c d -> [CIContent d] -> [IO AChatItem]
createACIs db itemTs createdAt cd = map $ \content -> do
ciId <- createNewChatItemNoMsg db user cd content itemTs createdAt
let ci = mkChatItem cd ciId content Nothing Nothing Nothing Nothing False itemTs Nothing createdAt
pure $ AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci
getCreateActiveUser :: SQLiteStore -> Bool -> IO User getCreateActiveUser :: SQLiteStore -> Bool -> IO User
getCreateActiveUser st testView = do getCreateActiveUser st testView = do
@@ -6133,10 +6218,14 @@ checkSameUser userId User {userId = activeUserId} = when (userId /= activeUserId
chatStarted :: ChatMonad m => m Bool chatStarted :: ChatMonad m => m Bool
chatStarted = fmap isJust . readTVarIO =<< asks agentAsync chatStarted = fmap isJust . readTVarIO =<< asks agentAsync
waitChatStarted :: ChatMonad m => m () waitChatStartedAndActivated :: ChatMonad m => m ()
waitChatStarted = do waitChatStartedAndActivated = do
agentStarted <- asks agentAsync agentStarted <- asks agentAsync
atomically $ readTVar agentStarted >>= \a -> unless (isJust a) retry chatActivated <- asks chatActivated
atomically $ do
started <- readTVar agentStarted
activated <- readTVar chatActivated
unless (isJust started && activated) retry
chatVersionRange :: ChatMonad' m => m VersionRange chatVersionRange :: ChatMonad' m => m VersionRange
chatVersionRange = do chatVersionRange = do
@@ -6173,8 +6262,8 @@ chatCommandP =
"/_delete user " *> (APIDeleteUser <$> A.decimal <* " del_smp=" <*> onOffP <*> optional (A.space *> jsonP)), "/_delete user " *> (APIDeleteUser <$> A.decimal <* " del_smp=" <*> onOffP <*> optional (A.space *> jsonP)),
"/delete user " *> (DeleteUser <$> displayName <*> pure True <*> optional (A.space *> pwdP)), "/delete user " *> (DeleteUser <$> displayName <*> pure True <*> optional (A.space *> pwdP)),
("/user" <|> "/u") $> ShowActiveUser, ("/user" <|> "/u") $> ShowActiveUser,
"/_start subscribe=" *> (StartChat <$> onOffP <* " expire=" <*> onOffP <* " xftp=" <*> onOffP), "/_start main=" *> (StartChat <$> onOffP),
"/_start" $> StartChat True True True, "/_start" $> StartChat True,
"/_stop" $> APIStopChat, "/_stop" $> APIStopChat,
"/_app activate restore=" *> (APIActivateChat <$> onOffP), "/_app activate restore=" *> (APIActivateChat <$> onOffP),
"/_app activate" $> APIActivateChat True, "/_app activate" $> APIActivateChat True,
+3 -3
View File
@@ -200,6 +200,7 @@ data ChatController = ChatController
expireCIThreads :: TMap UserId (Maybe (Async ())), expireCIThreads :: TMap UserId (Maybe (Async ())),
expireCIFlags :: TMap UserId Bool, expireCIFlags :: TMap UserId Bool,
cleanupManagerAsync :: TVar (Maybe (Async ())), cleanupManagerAsync :: TVar (Maybe (Async ())),
chatActivated :: TVar Bool,
timedItemThreads :: TMap (ChatRef, ChatItemId) (TVar (Maybe (Weak ThreadId))), timedItemThreads :: TMap (ChatRef, ChatItemId) (TVar (Maybe (Weak ThreadId))),
showLiveItems :: TVar Bool, showLiveItems :: TVar Bool,
encryptLocalFiles :: TVar Bool, encryptLocalFiles :: TVar Bool,
@@ -233,7 +234,7 @@ data ChatCommand
| UnmuteUser | UnmuteUser
| APIDeleteUser UserId Bool (Maybe UserPwd) | APIDeleteUser UserId Bool (Maybe UserPwd)
| DeleteUser UserName Bool (Maybe UserPwd) | DeleteUser UserName Bool (Maybe UserPwd)
| StartChat {subscribeConnections :: Bool, enableExpireChatItems :: Bool, startXFTPWorkers :: Bool} | StartChat {mainApp :: Bool}
| APIStopChat | APIStopChat
| APIActivateChat {restoreChat :: Bool} | APIActivateChat {restoreChat :: Bool}
| APISuspendChat {suspendTimeout :: Int} | APISuspendChat {suspendTimeout :: Int}
@@ -894,8 +895,7 @@ data PendingSubStatus = PendingSubStatus
deriving (Show) deriving (Show)
data UserProfileUpdateSummary = UserProfileUpdateSummary data UserProfileUpdateSummary = UserProfileUpdateSummary
{ notChanged :: Int, { updateSuccesses :: Int,
updateSuccesses :: Int,
updateFailures :: Int, updateFailures :: Int,
changedContacts :: [Contact] changedContacts :: [Contact]
} }
+1 -1
View File
@@ -35,7 +35,7 @@ runSimplexChat :: ChatOpts -> User -> ChatController -> (User -> ChatController
runSimplexChat ChatOpts {maintenance} u cc chat runSimplexChat ChatOpts {maintenance} u cc chat
| maintenance = wait =<< async (chat u cc) | maintenance = wait =<< async (chat u cc)
| otherwise = do | otherwise = do
a1 <- runReaderT (startChatController True True True) cc a1 <- runReaderT (startChatController True) cc
a2 <- async $ chat u cc a2 <- async $ chat u cc
waitEither_ a1 a2 waitEither_ a1 a2
+2 -2
View File
@@ -3,7 +3,7 @@
module Simplex.Chat.Mobile.Shared where module Simplex.Chat.Mobile.Shared where
import qualified Data.ByteString as B import qualified Data.ByteString as B
import Data.ByteString.Internal (ByteString (..), memcpy) import Data.ByteString.Internal (ByteString (..))
import qualified Data.ByteString.Lazy as LB import qualified Data.ByteString.Lazy as LB
import qualified Data.ByteString.Lazy.Internal as LB import qualified Data.ByteString.Lazy.Internal as LB
import Foreign import Foreign
@@ -21,7 +21,7 @@ getByteString ptr len = do
putByteString :: Ptr Word8 -> ByteString -> IO () putByteString :: Ptr Word8 -> ByteString -> IO ()
putByteString ptr (PS fp offset len) = putByteString ptr (PS fp offset len) =
withForeignPtr fp $ \p -> memcpy ptr (p `plusPtr` offset) len withForeignPtr fp $ \p -> copyBytes ptr (p `plusPtr` offset) len
{-# INLINE putByteString #-} {-# INLINE putByteString #-}
putLazyByteString :: Ptr Word8 -> LB.ByteString -> IO () putLazyByteString :: Ptr Word8 -> LB.ByteString -> IO ()
+16 -17
View File
@@ -460,24 +460,23 @@ createGroupInvitedViaLink
"INSERT INTO groups (group_profile_id, local_display_name, host_conn_custom_user_profile_id, user_id, enable_ntfs, created_at, updated_at, chat_ts) VALUES (?,?,?,?,?,?,?,?)" "INSERT INTO groups (group_profile_id, local_display_name, host_conn_custom_user_profile_id, user_id, enable_ntfs, created_at, updated_at, chat_ts) VALUES (?,?,?,?,?,?,?,?)"
(profileId, localDisplayName, customUserProfileId, userId, True, currentTs, currentTs, currentTs) (profileId, localDisplayName, customUserProfileId, userId, True, currentTs, currentTs, currentTs)
insertedRowId db insertedRowId db
insertHost_ currentTs groupId = ExceptT $ do insertHost_ currentTs groupId = do
let fromMemberProfile = profileFromName fromMemberName let fromMemberProfile = profileFromName fromMemberName
withLocalDisplayName db userId fromMemberName $ \localDisplayName -> runExceptT $ do (localDisplayName, profileId) <- createNewMemberProfile_ db user fromMemberProfile currentTs
(_, profileId) <- createNewMemberProfile_ db user fromMemberProfile currentTs let MemberIdRole {memberId, memberRole} = fromMember
let MemberIdRole {memberId, memberRole} = fromMember liftIO $ do
liftIO $ do DB.execute
DB.execute db
db [sql|
[sql| INSERT INTO group_members
INSERT INTO group_members ( group_id, member_id, member_role, member_category, member_status, invited_by,
( group_id, member_id, member_role, member_category, member_status, invited_by, user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at)
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?) |]
|] ( (groupId, memberId, memberRole, GCHostMember, GSMemAccepted, fromInvitedBy userContactId IBUnknown)
( (groupId, memberId, memberRole, GCHostMember, GSMemAccepted, fromInvitedBy userContactId IBUnknown) :. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs)
:. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs) )
) insertedRowId db
insertedRowId db
setViaGroupLinkHash :: DB.Connection -> GroupId -> Int64 -> IO () setViaGroupLinkHash :: DB.Connection -> GroupId -> Int64 -> IO ()
setViaGroupLinkHash db groupId connId = setViaGroupLinkHash db groupId connId =
+1
View File
@@ -415,6 +415,7 @@ xftpServerConfig =
logStatsStartTime = 0, logStatsStartTime = 0,
serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log", serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log",
serverStatsBackupFile = Nothing, serverStatsBackupFile = Nothing,
controlPort = Nothing,
transportConfig = defaultTransportServerConfig transportConfig = defaultTransportServerConfig
} }
+1 -1
View File
@@ -1149,7 +1149,7 @@ testSubscribeAppNSE tmp =
alice ##> "/_app suspend 1" alice ##> "/_app suspend 1"
alice <## "ok" alice <## "ok"
alice <## "chat suspended" alice <## "chat suspended"
nseAlice ##> "/_start subscribe=off expire=off xftp=off" nseAlice ##> "/_start main=off"
nseAlice <## "chat started" nseAlice <## "chat started"
nseAlice ##> "/ad" nseAlice ##> "/ad"
cLink <- getContactLink nseAlice True cLink <- getContactLink nseAlice True
+28
View File
@@ -67,6 +67,7 @@ chatProfileTests = do
xit'' "enable timed messages with contact" testEnableTimedMessagesContact xit'' "enable timed messages with contact" testEnableTimedMessagesContact
it "enable timed messages in group" testEnableTimedMessagesGroup it "enable timed messages in group" testEnableTimedMessagesGroup
xit'' "timed messages enabled globally, contact turns on" testTimedMessagesEnabledGlobally xit'' "timed messages enabled globally, contact turns on" testTimedMessagesEnabledGlobally
it "update multiple user preferences for multiple contacts" testUpdateMultipleUserPrefs
testUpdateProfile :: HasCallStack => FilePath -> IO () testUpdateProfile :: HasCallStack => FilePath -> IO ()
testUpdateProfile = testUpdateProfile =
@@ -1864,3 +1865,30 @@ testTimedMessagesEnabledGlobally =
bob <## "timed message deleted: hey" bob <## "timed message deleted: hey"
alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "Disappearing messages: enabled (1 sec)")]) alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "Disappearing messages: enabled (1 sec)")])
bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "Disappearing messages: enabled (1 sec)")]) bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "Disappearing messages: enabled (1 sec)")])
testUpdateMultipleUserPrefs :: HasCallStack => FilePath -> IO ()
testUpdateMultipleUserPrefs = testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
connectUsers alice bob
alice #> "@bob hi bob"
bob <# "alice> hi bob"
connectUsers alice cath
alice #> "@cath hi cath"
cath <# "alice> hi cath"
alice ##> "/_profile 1 {\"displayName\": \"alice\", \"fullName\": \"Alice\", \"preferences\": {\"fullDelete\": {\"allow\": \"always\"}, \"reactions\": {\"allow\": \"no\"}, \"receipts\": {\"allow\": \"yes\", \"activated\": true}}}"
alice <## "updated preferences:"
alice <## "Full deletion allowed: always"
alice <## "Message reactions allowed: no"
bob <## "alice updated preferences for you:"
bob <## "Full deletion: enabled for you (you allow: default (no), contact allows: always)"
bob <## "Message reactions: off (you allow: default (yes), contact allows: no)"
cath <## "alice updated preferences for you:"
cath <## "Full deletion: enabled for you (you allow: default (no), contact allows: always)"
cath <## "Message reactions: off (you allow: default (yes), contact allows: no)"
alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hi bob"), (1, "Full deletion: enabled for contact"), (1, "Message reactions: off")])
alice #$> ("/_get chat @3 count=100", chat, chatFeatures <> [(1, "hi cath"), (1, "Full deletion: enabled for contact"), (1, "Message reactions: off")])
+3 -2
View File
@@ -16,11 +16,12 @@ import qualified Data.Aeson.TH as JQ
import Data.ByteString (ByteString) import Data.ByteString (ByteString)
import qualified Data.ByteString as B import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
import Data.ByteString.Internal (create, memcpy) import Data.ByteString.Internal (create)
import qualified Data.ByteString.Lazy.Char8 as LB import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Word (Word8, Word32) import Data.Word (Word8, Word32)
import Foreign.C import Foreign.C
import Foreign.Marshal.Alloc (mallocBytes) import Foreign.Marshal.Alloc (mallocBytes)
import Foreign.Marshal.Utils (copyBytes)
import Foreign.Ptr import Foreign.Ptr
import Foreign.StablePtr import Foreign.StablePtr
import Foreign.Storable (peek) import Foreign.Storable (peek)
@@ -291,7 +292,7 @@ testFileCApi fileName tmp = do
peek ptr' `shouldReturn` (0 :: Word8) peek ptr' `shouldReturn` (0 :: Word8)
sz :: Word32 <- peek (ptr' `plusPtr` 1) sz :: Word32 <- peek (ptr' `plusPtr` 1)
let sz' = fromIntegral sz let sz' = fromIntegral sz
contents <- create sz' $ \toPtr -> memcpy toPtr (ptr' `plusPtr` 5) sz' contents <- create sz' $ \toPtr -> copyBytes toPtr (ptr' `plusPtr` 5) sz'
contents `shouldBe` src contents `shouldBe` src
sz' `shouldBe` fromIntegral len sz' `shouldBe` fromIntegral len