Compare commits

...

20 Commits

Author SHA1 Message Date
Evgeny Poberezkin 8159ae4aea 5.4.4: ios 190, android 172, desktop 24 2024-01-20 18:21:22 +00:00
Evgeny Poberezkin 5236e0f201 core: 5.4.4.0 2024-01-18 20:56:45 +00:00
Evgeny Poberezkin 1eb1ed92f7 ci: disable *-armv7a tags 2024-01-17 19:27:07 +00:00
Evgeny Poberezkin c25e9e3b72 scripts: curl/wget security (#3693) 2024-01-17 15:23:43 +00:00
Evgeny Poberezkin 300223b32e core: update simplexmq 5.5.0.6 (fix race conditions) (#3691)
* core: update simplexmq (fix race conditions)

* simplexmq 5.5.0.6
2024-01-16 23:42:29 +00:00
Evgeny Poberezkin 8d7dcb550a core: update simplexmq, optimize batching, remove builder (#3685)
* core: update simplexmq (optimize batching, remove builder)

* do not use builder to batch

* refactor
2024-01-15 10:46:13 +00:00
Evgeny Poberezkin 045b195483 5.4.3: ios 188, android 169, desktop 22 2024-01-10 11:56:57 +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 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 bb61b9c658 core: update simplexmq (critical errors, worker restarts, subscription timeouts) 2024-01-05 20:07:19 +00: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
57 changed files with 546 additions and 255 deletions
+1
View File
@@ -9,6 +9,7 @@ on:
tags: tags:
- "v*" - "v*"
- "!*-fdroid" - "!*-fdroid"
- "!*-armv7a"
pull_request: pull_request:
jobs: jobs:
+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) }
} }
} }
+2 -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)
} }
} }
+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)
@@ -134,20 +136,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",
+3 -3
View File
@@ -98,12 +98,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()
+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
} }
+32 -32
View File
@@ -43,11 +43,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 */; };
5C4E80DA2B3CCD090080FAE2 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80D52B3CCD090080FAE2 /* libgmp.a */; };
5C4E80DB2B3CCD090080FAE2 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80D62B3CCD090080FAE2 /* libffi.a */; };
5C4E80DC2B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80D72B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5.a */; };
5C4E80DD2B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80D82B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5-ghc9.6.3.a */; };
5C4E80DE2B3CCD090080FAE2 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80D92B3CCD090080FAE2 /* libgmpxx.a */; };
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; }; 5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; };
5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A91E283AD0E400C4E99E /* CallManager.swift */; }; 5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A91E283AD0E400C4E99E /* CallManager.swift */; };
5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */; }; 5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */; };
@@ -121,6 +116,11 @@
5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; }; 5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; };
5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */; }; 5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */; };
5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; }; 5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; };
5CCD2C322B5C29B400F76440 /* libHSsimplex-chat-5.4.4.0-F9Am8s51dKw4ZcRcMkpqsb-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD2C2D2B5C29B400F76440 /* libHSsimplex-chat-5.4.4.0-F9Am8s51dKw4ZcRcMkpqsb-ghc9.6.3.a */; };
5CCD2C332B5C29B400F76440 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD2C2E2B5C29B400F76440 /* libffi.a */; };
5CCD2C342B5C29B400F76440 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD2C2F2B5C29B400F76440 /* libgmp.a */; };
5CCD2C352B5C29B400F76440 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD2C302B5C29B400F76440 /* libgmpxx.a */; };
5CCD2C362B5C29B400F76440 /* libHSsimplex-chat-5.4.4.0-F9Am8s51dKw4ZcRcMkpqsb.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD2C312B5C29B400F76440 /* libHSsimplex-chat-5.4.4.0-F9Am8s51dKw4ZcRcMkpqsb.a */; };
5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; }; 5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; };
5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */; }; 5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */; };
5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -294,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>"; };
5C4E80D52B3CCD090080FAE2 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C4E80D62B3CCD090080FAE2 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C4E80D72B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5.a"; sourceTree = "<group>"; };
5C4E80D82B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5-ghc9.6.3.a"; sourceTree = "<group>"; };
5C4E80D92B3CCD090080FAE2 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = "<group>"; }; 5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = "<group>"; };
5C55A91E283AD0E400C4E99E /* CallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = "<group>"; }; 5C55A91E283AD0E400C4E99E /* CallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = "<group>"; };
5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = "<group>"; }; 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = "<group>"; };
@@ -408,6 +403,11 @@
5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; }; 5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIRcvDecryptionError.swift; sourceTree = "<group>"; }; 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIRcvDecryptionError.swift; sourceTree = "<group>"; };
5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = "<group>"; }; 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = "<group>"; };
5CCD2C2D2B5C29B400F76440 /* libHSsimplex-chat-5.4.4.0-F9Am8s51dKw4ZcRcMkpqsb-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.4.0-F9Am8s51dKw4ZcRcMkpqsb-ghc9.6.3.a"; sourceTree = "<group>"; };
5CCD2C2E2B5C29B400F76440 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5CCD2C2F2B5C29B400F76440 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5CCD2C302B5C29B400F76440 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5CCD2C312B5C29B400F76440 /* libHSsimplex-chat-5.4.4.0-F9Am8s51dKw4ZcRcMkpqsb.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.4.0-F9Am8s51dKw4ZcRcMkpqsb.a"; sourceTree = "<group>"; };
5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = "<group>"; }; 5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = "<group>"; };
5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanToConnectView.swift; sourceTree = "<group>"; }; 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanToConnectView.swift; sourceTree = "<group>"; };
5CD67B8D2B0E858A00C510B1 /* hs_init.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hs_init.h; sourceTree = "<group>"; }; 5CD67B8D2B0E858A00C510B1 /* hs_init.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hs_init.h; sourceTree = "<group>"; };
@@ -519,13 +519,13 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
5C4E80DD2B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5-ghc9.6.3.a in Frameworks */, 5CCD2C352B5C29B400F76440 /* libgmpxx.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5C4E80DA2B3CCD090080FAE2 /* libgmp.a in Frameworks */, 5CCD2C342B5C29B400F76440 /* libgmp.a in Frameworks */,
5C4E80DC2B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5.a in Frameworks */, 5CCD2C322B5C29B400F76440 /* libHSsimplex-chat-5.4.4.0-F9Am8s51dKw4ZcRcMkpqsb-ghc9.6.3.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
5C4E80DB2B3CCD090080FAE2 /* libffi.a in Frameworks */, 5CCD2C362B5C29B400F76440 /* libHSsimplex-chat-5.4.4.0-F9Am8s51dKw4ZcRcMkpqsb.a in Frameworks */,
5C4E80DE2B3CCD090080FAE2 /* libgmpxx.a in Frameworks */, 5CCD2C332B5C29B400F76440 /* libffi.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -587,11 +587,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = { 5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5C4E80D62B3CCD090080FAE2 /* libffi.a */, 5CCD2C2E2B5C29B400F76440 /* libffi.a */,
5C4E80D52B3CCD090080FAE2 /* libgmp.a */, 5CCD2C2F2B5C29B400F76440 /* libgmp.a */,
5C4E80D92B3CCD090080FAE2 /* libgmpxx.a */, 5CCD2C302B5C29B400F76440 /* libgmpxx.a */,
5C4E80D82B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5-ghc9.6.3.a */, 5CCD2C2D2B5C29B400F76440 /* libHSsimplex-chat-5.4.4.0-F9Am8s51dKw4ZcRcMkpqsb-ghc9.6.3.a */,
5C4E80D72B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5.a */, 5CCD2C312B5C29B400F76440 /* libHSsimplex-chat-5.4.4.0-F9Am8s51dKw4ZcRcMkpqsb.a */,
); );
path = Libraries; path = Libraries;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1518,7 +1518,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 186; CURRENT_PROJECT_VERSION = 190;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -1540,7 +1540,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 5.4.2; MARKETING_VERSION = 5.4.4;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX; PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -1561,7 +1561,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 186; CURRENT_PROJECT_VERSION = 190;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -1583,7 +1583,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 5.4.2; MARKETING_VERSION = 5.4.4;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX; PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -1642,7 +1642,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 186; CURRENT_PROJECT_VERSION = 190;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -1655,7 +1655,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 5.4.2; MARKETING_VERSION = 5.4.4;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1674,7 +1674,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 186; CURRENT_PROJECT_VERSION = 190;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -1687,7 +1687,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 5.4.2; MARKETING_VERSION = 5.4.4;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1706,7 +1706,7 @@
APPLICATION_EXTENSION_API_ONLY = YES; APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 186; CURRENT_PROJECT_VERSION = 190;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1730,7 +1730,7 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Libraries/sim", "$(PROJECT_DIR)/Libraries/sim",
); );
MARKETING_VERSION = 5.4.2; MARKETING_VERSION = 5.4.4;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -1752,7 +1752,7 @@
APPLICATION_EXTENSION_API_ONLY = YES; APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 186; CURRENT_PROJECT_VERSION = 190;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1776,7 +1776,7 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Libraries/sim", "$(PROJECT_DIR)/Libraries/sim",
); );
MARKETING_VERSION = 5.4.2; MARKETING_VERSION = 5.4.4;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -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")
} }
+2 -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)"
+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 */
@@ -164,13 +164,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() {
@@ -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")
} }
@@ -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()
} }
@@ -122,6 +122,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 {
@@ -108,6 +108,7 @@ class AppPreferences {
val chatArchiveTime = mkDatePreference(SHARED_PREFS_CHAT_ARCHIVE_TIME, null) val chatArchiveTime = mkDatePreference(SHARED_PREFS_CHAT_ARCHIVE_TIME, null)
val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null) val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null)
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_ONBOARDING_STAGE = "OnboardingStage" private const val SHARED_PREFS_ONBOARDING_STAGE = "OnboardingStage"
private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart" private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart"
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"
@@ -583,7 +585,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
@@ -1920,6 +1922,14 @@ object ChatController {
} }
} }
} }
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}")
} }
@@ -2161,7 +2171,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()
@@ -2288,7 +2298,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"
@@ -4710,6 +4720,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()
@@ -4721,6 +4732,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
@@ -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()
@@ -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,
@@ -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?) {
@@ -4,7 +4,6 @@ import SectionBottomSpacer
import SectionDividerSpaced import SectionDividerSpaced
import SectionTextFooter import SectionTextFooter
import SectionItemView import SectionItemView
import SectionSpacer
import SectionView import SectionView
import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@@ -367,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) {
@@ -407,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),
@@ -414,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),
@@ -422,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 +435,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())
@@ -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),
)
}
}
}
}
@@ -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(
@@ -662,6 +662,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>
@@ -1724,4 +1725,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">You are already in group %1$s.</string> <string name="connect_plan_you_are_already_in_group_vName">You are already in group %1$s.</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 }
@@ -16,13 +16,14 @@ 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()
withBGApi { withBGApi {
@@ -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)
}
}
}
}
+4 -4
View File
@@ -25,11 +25,11 @@ android.nonTransitiveRClass=true
android.enableJetifier=true android.enableJetifier=true
kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.mpp.androidSourceSetLayoutVersion=2
android.version_name=5.4.2 android.version_name=5.4.4
android.version_code=166 android.version_code=172
desktop.version_name=5.4.2 desktop.version_name=5.4.4
desktop.version_code=20 desktop.version_code=24
kotlin.version=1.8.20 kotlin.version=1.8.20
gradle.plugin.version=7.4.2 gradle.plugin.version=7.4.2
+1 -1
View File
@@ -14,7 +14,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: f6ed4640d407f8879273d104a3e69069806dcb7c
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.4.2.1 version: 5.4.4.0
#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 --tlsv1.2 --location -o libsupport.zip $job_repo/$arch-android:lib:support.x86_64-linux/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 --tlsv1.2 --location -o libsimplex.zip "$job_repo"/"$arch"-android:lib:simplex-chat.x86_64-linux/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
+1 -1
View File
@@ -32,7 +32,7 @@ mkdir deps 2> /dev/null || true
cp /tmp/libffi-3.4.4/*-apple-darwin*/.libs/libffi.dylib $BUILD/deps || \ cp /tmp/libffi-3.4.4/*-apple-darwin*/.libs/libffi.dylib $BUILD/deps || \
( \ ( \
cd /tmp && \ cd /tmp && \
curl "https://gitlab.haskell.org/ghc/libffi-tarballs/-/raw/libffi-3.4.4/libffi-3.4.4.tar.gz?inline=false" -o libffi.tar.gz && \ curl --tlsv1.2 "https://gitlab.haskell.org/ghc/libffi-tarballs/-/raw/libffi-3.4.4/libffi-3.4.4.tar.gz?inline=false" -o libffi.tar.gz && \
tar -xzvf libffi.tar.gz && \ tar -xzvf libffi.tar.gz && \
cd "libffi-3.4.4" && \ cd "libffi-3.4.4" && \
./configure && \ ./configure && \
+1 -1
View File
@@ -37,7 +37,7 @@ cp *imple*.desktop usr/share/applications/
cp $multiplatform_dir/desktop/src/jvmMain/resources/distribute/*.appdata.xml usr/share/metainfo cp $multiplatform_dir/desktop/src/jvmMain/resources/distribute/*.appdata.xml usr/share/metainfo
if [ ! -f ../appimagetool-x86_64.AppImage ]; then if [ ! -f ../appimagetool-x86_64.AppImage ]; then
wget https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O ../appimagetool-x86_64.AppImage wget --secure-protocol=TLSv1_3 https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O ../appimagetool-x86_64.AppImage
chmod +x ../appimagetool-x86_64.AppImage chmod +x ../appimagetool-x86_64.AppImage
fi fi
../appimagetool-x86_64.AppImage . ../appimagetool-x86_64.AppImage .
+1 -1
View File
@@ -12,7 +12,7 @@ cd $root_dir
if [ ! -f dist-newstyle/openssl-1.1.1w/libcrypto-1_1-x64.dll ]; then if [ ! -f dist-newstyle/openssl-1.1.1w/libcrypto-1_1-x64.dll ]; then
mkdir dist-newstyle 2>/dev/null || true mkdir dist-newstyle 2>/dev/null || true
cd dist-newstyle cd dist-newstyle
curl https://www.openssl.org/source/openssl-1.1.1w.tar.gz -o openssl.tar.gz curl --tlsv1.2 https://www.openssl.org/source/openssl-1.1.1w.tar.gz -o openssl.tar.gz
$WINDIR\\System32\\tar.exe -xvzf openssl.tar.gz $WINDIR\\System32\\tar.exe -xvzf openssl.tar.gz
cd openssl-1.1.1w cd openssl-1.1.1w
./Configure mingw64 ./Configure mingw64
+5 -5
View File
@@ -14,7 +14,7 @@ mkdir $vlc_dir || exit 0
cd /tmp cd /tmp
mkdir tmp 2>/dev/null || true mkdir tmp 2>/dev/null || true
cd tmp cd tmp
curl https://github.com/cmatomic/VLCplayer-AppImage/releases/download/3.0.11.1/VLC_media_player-3.0.11.1-x86_64.AppImage -L -o appimage curl --tlsv1.2 https://github.com/cmatomic/VLCplayer-AppImage/releases/download/3.0.11.1/VLC_media_player-3.0.11.1-x86_64.AppImage -L -o appimage
chmod +x appimage chmod +x appimage
./appimage --appimage-extract ./appimage --appimage-extract
cp -r squashfs-root/usr/lib/* $vlc_dir cp -r squashfs-root/usr/lib/* $vlc_dir
@@ -28,7 +28,7 @@ cd /tmp
( (
mkdir tmp mkdir tmp
cd tmp cd tmp
curl http://archive.ubuntu.com/ubuntu/pool/universe/v/vlc/libvlc5_3.0.9.2-1_amd64.deb -o libvlc curl --tlsv1.2 https://archive.ubuntu.com/ubuntu/pool/universe/v/vlc/libvlc5_3.0.9.2-1_amd64.deb -o libvlc
ar p libvlc data.tar.xz > data.tar.xz ar p libvlc data.tar.xz > data.tar.xz
tar -xvf data.tar.xz tar -xvf data.tar.xz
mv usr/lib/x86_64-linux-gnu/libvlc.so{.5,} mv usr/lib/x86_64-linux-gnu/libvlc.so{.5,}
@@ -40,7 +40,7 @@ rm -rf tmp
( (
mkdir tmp mkdir tmp
cd tmp cd tmp
curl http://archive.ubuntu.com/ubuntu/pool/universe/v/vlc/libvlccore9_3.0.9.2-1_amd64.deb -o libvlccore curl --tlsv1.2 https://archive.ubuntu.com/ubuntu/pool/universe/v/vlc/libvlccore9_3.0.9.2-1_amd64.deb -o libvlccore
ar p libvlccore data.tar.xz > data.tar.xz ar p libvlccore data.tar.xz > data.tar.xz
tar -xvf data.tar.xz tar -xvf data.tar.xz
cp usr/lib/x86_64-linux-gnu/libvlccore.so* $vlc_dir cp usr/lib/x86_64-linux-gnu/libvlccore.so* $vlc_dir
@@ -51,7 +51,7 @@ rm -rf tmp
( (
mkdir tmp mkdir tmp
cd tmp cd tmp
curl http://mirrors.edge.kernel.org/ubuntu/pool/universe/v/vlc/vlc-plugin-base_3.0.9.2-1_amd64.deb -o plugins curl --tlsv1.2 https://mirrors.edge.kernel.org/ubuntu/pool/universe/v/vlc/vlc-plugin-base_3.0.9.2-1_amd64.deb -o plugins
ar p plugins data.tar.xz > data.tar.xz ar p plugins data.tar.xz > data.tar.xz
tar -xvf data.tar.xz tar -xvf data.tar.xz
find usr/lib/x86_64-linux-gnu/vlc/plugins/ -name "lib*.so*" -exec patchelf --set-rpath '$ORIGIN/../../' {} \; find usr/lib/x86_64-linux-gnu/vlc/plugins/ -name "lib*.so*" -exec patchelf --set-rpath '$ORIGIN/../../' {} \;
@@ -63,7 +63,7 @@ rm -rf tmp
( (
mkdir tmp mkdir tmp
cd tmp cd tmp
curl http://archive.ubuntu.com/ubuntu/pool/main/libi/libidn/libidn11_1.33-2.2ubuntu2_amd64.deb -o idn curl --tlsv1.2 https://archive.ubuntu.com/ubuntu/pool/main/libi/libidn/libidn11_1.33-2.2ubuntu2_amd64.deb -o idn
ar p idn data.tar.xz > data.tar.xz ar p idn data.tar.xz > data.tar.xz
tar -xvf data.tar.xz tar -xvf data.tar.xz
cp lib/x86_64-linux-gnu/lib* $vlc_dir cp lib/x86_64-linux-gnu/lib* $vlc_dir
+1 -1
View File
@@ -23,7 +23,7 @@ mkdir -p $vlc_dir/vlc || exit 0
cd /tmp cd /tmp
mkdir tmp 2>/dev/null || true mkdir tmp 2>/dev/null || true
cd tmp cd tmp
curl https://github.com/simplex-chat/vlc/releases/download/v$vlc_version/vlc-macos-$ARCH.zip -L -o vlc curl --tlsv1.2 https://github.com/simplex-chat/vlc/releases/download/v$vlc_version/vlc-macos-$ARCH.zip -L -o vlc
unzip -oqq vlc unzip -oqq vlc
install_name_tool -add_rpath "@loader_path/VLC.app/Contents/MacOS/lib" vlc-cache-gen install_name_tool -add_rpath "@loader_path/VLC.app/Contents/MacOS/lib" vlc-cache-gen
cd VLC.app/Contents/MacOS/lib cd VLC.app/Contents/MacOS/lib
+1 -1
View File
@@ -13,7 +13,7 @@ mkdir -p $vlc_dir/vlc || exit 0
cd /tmp cd /tmp
mkdir tmp 2>/dev/null || true mkdir tmp 2>/dev/null || true
cd tmp cd tmp
curl https://irltoolkit.mm.fcix.net/videolan-ftp/vlc/3.0.18/win64/vlc-3.0.18-win64.zip -L -o vlc curl --tlsv1.2 https://irltoolkit.mm.fcix.net/videolan-ftp/vlc/3.0.18/win64/vlc-3.0.18-win64.zip -L -o vlc
$WINDIR\\System32\\tar.exe -xf vlc $WINDIR\\System32\\tar.exe -xf vlc
cd vlc-* cd vlc-*
# Setting the same date as the date that will be on the file after extraction from JAR to make VLC cache checker happy # Setting the same date as the date that will be on the file after extraction from JAR to make VLC cache checker happy
+1 -1
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 --tlsv1.2 --location -o "$output_dir"/pkg-ios-"$arch"-swift-json.zip "$job_repo"/"$arch"-darwin-ios:lib:simplex-chat."$arch"-darwin/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"."f6ed4640d407f8879273d104a3e69069806dcb7c" = "072rakv697f85i8ldjl7bj7jc7vfmzphasx2i4ynwgz3kksydfp5";
"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.4.2.1 version: 5.4.4.0
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
+37 -26
View File
@@ -29,7 +29,6 @@ import Data.Bifunctor (bimap, first)
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
import Data.ByteString.Builder (toLazyByteString)
import Data.ByteString.Char8 (ByteString) import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB import qualified Data.ByteString.Lazy.Char8 as LB
@@ -234,6 +233,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
@@ -269,6 +269,7 @@ newChatController
expireCIFlags, expireCIFlags,
cleanupManagerAsync, cleanupManagerAsync,
timedItemThreads, timedItemThreads,
chatActivated,
showLiveItems, showLiveItems,
encryptLocalFiles, encryptLocalFiles,
userXFTPFileConfig, userXFTPFileConfig,
@@ -311,10 +312,10 @@ cfgServers p s = case p of
SPSMP -> s.smp SPSMP -> s.smp
SPXFTP -> s.xftp SPXFTP -> s.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 +325,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
@@ -544,16 +545,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 +563,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)
@@ -2479,6 +2482,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
@@ -2972,7 +2976,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
@@ -2982,7 +2986,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))
@@ -3037,7 +3041,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
@@ -3063,8 +3067,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
@@ -3083,11 +3089,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
@@ -5647,8 +5655,7 @@ sendGroupMemberMessages user conn@Connection {connId} events groupId = do
processBatch batch `catchChatError` (toView . CRChatError (Just user)) processBatch batch `catchChatError` (toView . CRChatError (Just user))
where where
processBatch :: MsgBatch -> m () processBatch :: MsgBatch -> m ()
processBatch (MsgBatch builder sndMsgs) = do processBatch (MsgBatch batchBody sndMsgs) = do
let batchBody = LB.toStrict $ toLazyByteString builder
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
@@ -5668,28 +5675,28 @@ directMessage chatMsgEvent = do
chatVRange <- chatVersionRange chatVRange <- chatVersionRange
let r = encodeChatMessage ChatMessage {chatVRange, msgId = Nothing, chatMsgEvent} let r = encodeChatMessage ChatMessage {chatVRange, msgId = Nothing, chatMsgEvent}
case r of case r of
ECMEncoded encodedBody -> pure . LB.toStrict $ encodedBody ECMEncoded encodedBody -> pure encodedBody
ECMLarge -> throwChatError $ CEException "large message" ECMLarge -> throwChatError $ CEException "large message"
deliverMessage :: ChatMonad m => Connection -> CMEventTag e -> LazyMsgBody -> MessageId -> m Int64 deliverMessage :: ChatMonad m => Connection -> CMEventTag e -> MsgBody -> MessageId -> m Int64
deliverMessage conn cmEventTag msgBody msgId = do deliverMessage conn cmEventTag msgBody msgId = do
let msgFlags = MsgFlags {notification = hasNotification cmEventTag} let msgFlags = MsgFlags {notification = hasNotification cmEventTag}
deliverMessage' conn msgFlags msgBody msgId deliverMessage' conn msgFlags msgBody msgId
deliverMessage' :: ChatMonad m => Connection -> MsgFlags -> LazyMsgBody -> MessageId -> m Int64 deliverMessage' :: ChatMonad m => Connection -> MsgFlags -> MsgBody -> MessageId -> m Int64
deliverMessage' conn msgFlags msgBody msgId = deliverMessage' conn msgFlags msgBody msgId =
deliverMessages [(conn, msgFlags, msgBody, msgId)] >>= \case deliverMessages [(conn, msgFlags, msgBody, msgId)] >>= \case
[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] deliverMessages :: ChatMonad' m => [(Connection, MsgFlags, MsgBody, MessageId)] -> m [Either ChatError Int64]
deliverMessages msgReqs = do deliverMessages msgReqs = do
sent <- zipWith prepareBatch msgReqs <$> withAgent' (`sendMessages` aReqs) sent <- zipWith prepareBatch msgReqs <$> withAgent' (`sendMessages` aReqs)
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 aReqs = map (\(conn, msgFlags, msgBody, _msgId) -> (aConnId conn, msgFlags, msgBody)) msgReqs
prepareBatch req = bimap (`ChatErrorAgent` Nothing) (req,) prepareBatch req = bimap (`ChatErrorAgent` Nothing) (req,)
createDelivery :: DB.Connection -> ((Connection, MsgFlags, LazyMsgBody, MessageId), AgentMsgId) -> IO (Either ChatError Int64) createDelivery :: DB.Connection -> ((Connection, MsgFlags, MsgBody, MessageId), 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
@@ -6113,10 +6120,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
@@ -6153,8 +6164,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,
+2 -1
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}
+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 -5
View File
@@ -22,7 +22,6 @@ import qualified Data.Aeson.Encoding as JE
import qualified Data.Aeson.TH as JQ import qualified Data.Aeson.TH as JQ
import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Lazy as L
import qualified Data.ByteString.Lazy.Char8 as LB import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Char (isSpace) import Data.Char (isSpace)
import Data.Int (Int64) import Data.Int (Int64)
@@ -764,12 +763,10 @@ checkChatType x = case testEquality (chatTypeI @c) (chatTypeI @c') of
Just Refl -> Right x Just Refl -> Right x
Nothing -> Left "bad chat type" Nothing -> Left "bad chat type"
type LazyMsgBody = L.ByteString
data SndMessage = SndMessage data SndMessage = SndMessage
{ msgId :: MessageId, { msgId :: MessageId,
sharedMsgId :: SharedMsgId, sharedMsgId :: SharedMsgId,
msgBody :: LazyMsgBody msgBody :: MsgBody
} }
deriving (Show) deriving (Show)
@@ -791,7 +788,7 @@ data RcvMessage = RcvMessage
data PendingGroupMessage = PendingGroupMessage data PendingGroupMessage = PendingGroupMessage
{ msgId :: MessageId, { msgId :: MessageId,
cmEventTag :: ACMEventTag, cmEventTag :: ACMEventTag,
msgBody :: LazyMsgBody, msgBody :: MsgBody,
introId_ :: Maybe Int64 introId_ :: Maybe Int64
} }
+20 -23
View File
@@ -1,6 +1,7 @@
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
module Simplex.Chat.Messages.Batch module Simplex.Chat.Messages.Batch
@@ -9,33 +10,29 @@ module Simplex.Chat.Messages.Batch
) )
where where
import Data.ByteString.Builder (Builder, charUtf8, lazyByteString) import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Lazy as LB import qualified Data.ByteString.Char8 as B
import Data.Int (Int64)
import Simplex.Chat.Controller (ChatError (..), ChatErrorType (..)) import Simplex.Chat.Controller (ChatError (..), ChatErrorType (..))
import Simplex.Chat.Messages import Simplex.Chat.Messages
data MsgBatch = MsgBatch Builder [SndMessage] data MsgBatch = MsgBatch ByteString [SndMessage]
deriving (Show) deriving (Show)
-- | Batches [SndMessage] into batches of ByteString builders in form of JSON arrays. -- | Batches [SndMessage] into batches of ByteStrings in form of JSON arrays.
-- Does not check if the resulting batch is a valid JSON. -- Does not check if the resulting batch is a valid JSON.
-- If a single element is passed, it is returned as is (a JSON string). -- If a single element is passed, it is returned as is (a JSON string).
-- If an element exceeds maxLen, it is returned as ChatError. -- If an element exceeds maxLen, it is returned as ChatError.
batchMessages :: Int64 -> [SndMessage] -> [Either ChatError MsgBatch] batchMessages :: Int -> [SndMessage] -> [Either ChatError MsgBatch]
batchMessages maxLen msgs = batchMessages maxLen = addBatch . foldr addToBatch ([], [], 0, 0)
let (batches, batch, _, n) = foldr addToBatch ([], [], 0, 0) msgs
in if n == 0 then batches else msgBatch batch : batches
where where
msgBatch batch = Right (MsgBatch (encodeMessages batch) batch) msgBatch batch = Right (MsgBatch (encodeMessages batch) batch)
addToBatch :: SndMessage -> ([Either ChatError MsgBatch], [SndMessage], Int64, Int) -> ([Either ChatError MsgBatch], [SndMessage], Int64, Int) addToBatch :: SndMessage -> ([Either ChatError MsgBatch], [SndMessage], Int, Int) -> ([Either ChatError MsgBatch], [SndMessage], Int, Int)
addToBatch msg@SndMessage {msgBody} (batches, batch, len, n) addToBatch msg@SndMessage {msgBody} acc@(batches, batch, len, n)
| batchLen <= maxLen = (batches, msg : batch, len', n + 1) | batchLen <= maxLen = (batches, msg : batch, len', n + 1)
| msgLen <= maxLen = (batches', [msg], msgLen, 1) | msgLen <= maxLen = (addBatch acc, [msg], msgLen, 1)
| otherwise = (errLarge msg : (if n == 0 then batches else batches'), [], 0, 0) | otherwise = (errLarge msg : addBatch acc, [], 0, 0)
where where
msgLen = LB.length msgBody msgLen = B.length msgBody
batches' = msgBatch batch : batches
len' len'
| n == 0 = msgLen | n == 0 = msgLen
| otherwise = msgLen + len + 1 -- 1 accounts for comma | otherwise = msgLen + len + 1 -- 1 accounts for comma
@@ -43,11 +40,11 @@ batchMessages maxLen msgs =
| n == 0 = len' | n == 0 = len'
| otherwise = len' + 2 -- 2 accounts for opening and closing brackets | otherwise = len' + 2 -- 2 accounts for opening and closing brackets
errLarge SndMessage {msgId} = Left $ ChatError $ CEInternalError ("large message " <> show msgId) errLarge SndMessage {msgId} = Left $ ChatError $ CEInternalError ("large message " <> show msgId)
addBatch :: ([Either ChatError MsgBatch], [SndMessage], Int, Int) -> [Either ChatError MsgBatch]
encodeMessages :: [SndMessage] -> Builder addBatch (batches, batch, _, n) = if n == 0 then batches else msgBatch batch : batches
encodeMessages = \case encodeMessages :: [SndMessage] -> ByteString
[] -> mempty encodeMessages = \case
[msg] -> encodeMsg msg [] -> mempty
(msg : msgs) -> charUtf8 '[' <> encodeMsg msg <> mconcat [charUtf8 ',' <> encodeMsg msg' | msg' <- msgs] <> charUtf8 ']' [msg] -> body msg
where msgs -> B.concat ["[", B.intercalate "," (map body msgs), "]"]
encodeMsg SndMessage {msgBody} = lazyByteString msgBody body SndMessage {msgBody} = msgBody
+5 -7
View File
@@ -29,9 +29,7 @@ import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.ByteString.Char8 (ByteString) import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Char8 as B
import Data.ByteString.Internal (c2w, w2c) import Data.ByteString.Internal (c2w, w2c)
import qualified Data.ByteString.Lazy as L
import qualified Data.ByteString.Lazy.Char8 as LB import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Int (Int64)
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Data.String import Data.String
import Data.Text (Text) import Data.Text (Text)
@@ -491,20 +489,20 @@ $(JQ.deriveJSON defaultJSON ''QuotedMsg)
-- this limit reserves space for metadata in forwarded messages -- this limit reserves space for metadata in forwarded messages
-- 15780 (limit used for fileChunkSize) - 161 (x.grp.msg.forward overhead) = 15619, round to 15610 -- 15780 (limit used for fileChunkSize) - 161 (x.grp.msg.forward overhead) = 15619, round to 15610
maxChatMsgSize :: Int64 maxChatMsgSize :: Int
maxChatMsgSize = 15610 maxChatMsgSize = 15610
data EncodedChatMessage = ECMEncoded L.ByteString | ECMLarge data EncodedChatMessage = ECMEncoded ByteString | ECMLarge
encodeChatMessage :: MsgEncodingI e => ChatMessage e -> EncodedChatMessage encodeChatMessage :: MsgEncodingI e => ChatMessage e -> EncodedChatMessage
encodeChatMessage msg = do encodeChatMessage msg = do
case chatToAppMessage msg of case chatToAppMessage msg of
AMJson m -> do AMJson m -> do
let body = J.encode m let body = LB.toStrict $ J.encode m
if LB.length body > maxChatMsgSize if B.length body > maxChatMsgSize
then ECMLarge then ECMLarge
else ECMEncoded body else ECMEncoded body
AMBinary m -> ECMEncoded . LB.fromStrict $ strEncode m AMBinary m -> ECMEncoded $ strEncode m
parseChatMessages :: ByteString -> [Either String AChatMessage] parseChatMessages :: ByteString -> [Either String AChatMessage]
parseChatMessages "" = [Left "empty string"] parseChatMessages "" = [Left "empty string"]
+1
View File
@@ -413,6 +413,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
+8 -8
View File
@@ -7,8 +7,8 @@
module MessageBatching (batchingTests) where module MessageBatching (batchingTests) where
import Crypto.Number.Serialize (os2ip) import Crypto.Number.Serialize (os2ip)
import Data.ByteString.Builder (toLazyByteString) import Data.ByteString (ByteString)
import qualified Data.ByteString.Lazy as LB import qualified Data.ByteString as B
import Data.Either (partitionEithers) import Data.Either (partitionEithers)
import Data.Int (Int64) import Data.Int (Int64)
import Data.String (IsString (..)) import Data.String (IsString (..))
@@ -26,7 +26,7 @@ batchingTests = describe "message batching tests" $ do
it "image x.msg.new and x.msg.file.descr should fit into single batch" testImageFitsSingleBatch it "image x.msg.new and x.msg.file.descr should fit into single batch" testImageFitsSingleBatch
instance IsString SndMessage where instance IsString SndMessage where
fromString s = SndMessage {msgId, sharedMsgId = SharedMsgId "", msgBody = LB.fromStrict s'} fromString s = SndMessage {msgId, sharedMsgId = SharedMsgId "", msgBody = s'}
where where
s' = encodeUtf8 $ T.pack s s' = encodeUtf8 $ T.pack s
msgId = fromInteger $ os2ip s' msgId = fromInteger $ os2ip s'
@@ -94,14 +94,14 @@ testImageFitsSingleBatch = do
-- 261_120 bytes (MAX_IMAGE_SIZE in UI), rounded up, example was 743 -- 261_120 bytes (MAX_IMAGE_SIZE in UI), rounded up, example was 743
let descrRoundedSize = 800 let descrRoundedSize = 800
let xMsgNewStr = LB.replicate xMsgNewRoundedSize 1 let xMsgNewStr = B.replicate xMsgNewRoundedSize 1
descrStr = LB.replicate descrRoundedSize 2 descrStr = B.replicate descrRoundedSize 2
msg s = SndMessage {msgId = 0, sharedMsgId = SharedMsgId "", msgBody = s} msg s = SndMessage {msgId = 0, sharedMsgId = SharedMsgId "", msgBody = s}
batched = "[" <> xMsgNewStr <> "," <> descrStr <> "]" batched = "[" <> xMsgNewStr <> "," <> descrStr <> "]"
runBatcherTest' maxChatMsgSize [msg xMsgNewStr, msg descrStr] [] [batched] runBatcherTest' maxChatMsgSize [msg xMsgNewStr, msg descrStr] [] [batched]
runBatcherTest :: Int64 -> [SndMessage] -> [ChatError] -> [LB.ByteString] -> Spec runBatcherTest :: Int -> [SndMessage] -> [ChatError] -> [ByteString] -> Spec
runBatcherTest maxLen msgs expectedErrors expectedBatches = runBatcherTest maxLen msgs expectedErrors expectedBatches =
it it
( (show (map (\SndMessage {msgBody} -> msgBody) msgs) <> ", limit " <> show maxLen <> ": should return ") ( (show (map (\SndMessage {msgBody} -> msgBody) msgs) <> ", limit " <> show maxLen <> ": should return ")
@@ -110,10 +110,10 @@ runBatcherTest maxLen msgs expectedErrors expectedBatches =
) )
(runBatcherTest' maxLen msgs expectedErrors expectedBatches) (runBatcherTest' maxLen msgs expectedErrors expectedBatches)
runBatcherTest' :: Int64 -> [SndMessage] -> [ChatError] -> [LB.ByteString] -> IO () runBatcherTest' :: Int -> [SndMessage] -> [ChatError] -> [ByteString] -> IO ()
runBatcherTest' maxLen msgs expectedErrors expectedBatches = do runBatcherTest' maxLen msgs expectedErrors expectedBatches = do
let (errors, batches) = partitionEithers $ batchMessages maxLen msgs let (errors, batches) = partitionEithers $ batchMessages maxLen msgs
batchedStrs = map (\(MsgBatch builder _) -> toLazyByteString builder) batches batchedStrs = map (\(MsgBatch batchBody _) -> batchBody) batches
testErrors errors `shouldBe` testErrors expectedErrors testErrors errors `shouldBe` testErrors expectedErrors
batchedStrs `shouldBe` expectedBatches batchedStrs `shouldBe` expectedBatches
where where
+1 -2
View File
@@ -7,7 +7,6 @@ module ProtocolTests where
import qualified Data.Aeson as J import qualified Data.Aeson as J
import Data.ByteString.Char8 (ByteString) import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Time.Clock.System (SystemTime (..), systemToUTCTime) import Data.Time.Clock.System (SystemTime (..), systemToUTCTime)
import Simplex.Chat.Protocol import Simplex.Chat.Protocol
import Simplex.Chat.Types import Simplex.Chat.Types
@@ -74,7 +73,7 @@ s ##== msg = do
let r = encodeChatMessage msg let r = encodeChatMessage msg
case r of case r of
ECMEncoded encodedBody -> ECMEncoded encodedBody ->
J.eitherDecodeStrict' (LB.toStrict encodedBody) J.eitherDecodeStrict' encodedBody
`shouldBe` (J.eitherDecodeStrict' s :: Either String J.Value) `shouldBe` (J.eitherDecodeStrict' s :: Either String J.Value)
ECMLarge -> expectationFailure $ "large message" ECMLarge -> expectationFailure $ "large message"