ios: communication between NSE and app via files (#3533)
* ios: communication between NSE and app via files * clean up * better concurrency
This commit is contained in:
committed by
GitHub
parent
e8016adfdc
commit
79a954336c
83
apps/ios/Shared/Model/NSESubscriber.swift
Normal file
83
apps/ios/Shared/Model/NSESubscriber.swift
Normal file
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// NSESubscriber.swift
|
||||
// SimpleXChat
|
||||
//
|
||||
// Created by Evgeny on 09/12/2023.
|
||||
// Copyright © 2023 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SimpleXChat
|
||||
|
||||
private var nseSubscribers: [UUID:NSESubscriber] = [:]
|
||||
|
||||
// timeout for active notification service extension going into "suspending" state.
|
||||
// If in two seconds the state does not change, we assume that it was not running and proceed with app activation/answering call.
|
||||
private let SUSPENDING_TIMEOUT: TimeInterval = 2
|
||||
|
||||
// timeout should be larger than SUSPENDING_TIMEOUT
|
||||
func waitNSESuspended(timeout: TimeInterval, dispatchQueue: DispatchQueue = DispatchQueue.main, suspended: @escaping (Bool) -> Void) {
|
||||
if timeout <= SUSPENDING_TIMEOUT {
|
||||
logger.warning("waitNSESuspended: small timeout \(timeout), using \(SUSPENDING_TIMEOUT + 1)")
|
||||
}
|
||||
var state = nseStateGroupDefault.get()
|
||||
if case .suspended = state {
|
||||
dispatchQueue.async { suspended(true) }
|
||||
return
|
||||
}
|
||||
let id = UUID()
|
||||
var suspendedCalled = false
|
||||
checkTimeout()
|
||||
nseSubscribers[id] = nseMessageSubscriber { msg in
|
||||
if case let .state(newState) = msg {
|
||||
state = newState
|
||||
logger.debug("waitNSESuspended state: \(state.rawValue)")
|
||||
if case .suspended = newState {
|
||||
notifySuspended(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
|
||||
func notifySuspended(_ ok: Bool) {
|
||||
logger.debug("waitNSESuspended notifySuspended: \(ok)")
|
||||
if !suspendedCalled {
|
||||
logger.debug("waitNSESuspended notifySuspended: calling suspended(\(ok))")
|
||||
suspendedCalled = true
|
||||
nseSubscribers.removeValue(forKey: id)
|
||||
dispatchQueue.async { suspended(ok) }
|
||||
}
|
||||
}
|
||||
|
||||
func checkTimeout() {
|
||||
if !suspending() {
|
||||
checkSuspendingTimeout()
|
||||
} else if state == .suspending {
|
||||
checkSuspendedTimeout()
|
||||
}
|
||||
}
|
||||
|
||||
func suspending() -> Bool {
|
||||
suspendedCalled || state == .suspended || state == .suspending
|
||||
}
|
||||
|
||||
func checkSuspendingTimeout() {
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + SUSPENDING_TIMEOUT) {
|
||||
logger.debug("waitNSESuspended check suspending timeout")
|
||||
if !suspending() {
|
||||
notifySuspended(false)
|
||||
} else if state != .suspended {
|
||||
checkSuspendedTimeout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkSuspendedTimeout() {
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + min(timeout - SUSPENDING_TIMEOUT, 1)) {
|
||||
logger.debug("waitNSESuspended check suspended timeout")
|
||||
if state != .suspended {
|
||||
notifySuspended(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,26 +12,24 @@ import SimpleXChat
|
||||
|
||||
private let suspendLockQueue = DispatchQueue(label: "chat.simplex.app.suspend.lock")
|
||||
|
||||
let appSuspendTimeout: Int = 15 // seconds
|
||||
|
||||
let bgSuspendTimeout: Int = 5 // seconds
|
||||
|
||||
let terminationTimeout: Int = 3 // seconds
|
||||
|
||||
let activationDelay: Double = 1.5 // seconds
|
||||
let activationDelay: TimeInterval = 1.5
|
||||
|
||||
private func _suspendChat(timeout: Int) {
|
||||
// this is a redundant check to prevent logical errors, like the one fixed in this PR
|
||||
let state = appStateGroupDefault.get()
|
||||
let state = AppChatState.shared.value
|
||||
if !state.canSuspend {
|
||||
logger.error("_suspendChat called, current state: \(state.rawValue, privacy: .public)")
|
||||
} else if ChatModel.ok {
|
||||
appStateGroupDefault.set(.suspending)
|
||||
AppChatState.shared.set(.suspending)
|
||||
apiSuspendChat(timeoutMicroseconds: timeout * 1000000)
|
||||
let endTask = beginBGTask(chatSuspended)
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + Double(timeout) + 1, execute: endTask)
|
||||
} else {
|
||||
appStateGroupDefault.set(.suspended)
|
||||
AppChatState.shared.set(.suspended)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +41,7 @@ func suspendChat() {
|
||||
|
||||
func suspendBgRefresh() {
|
||||
suspendLockQueue.sync {
|
||||
if case .bgRefresh = appStateGroupDefault.get() {
|
||||
if case .bgRefresh = AppChatState.shared.value {
|
||||
_suspendChat(timeout: bgSuspendTimeout)
|
||||
}
|
||||
}
|
||||
@@ -52,7 +50,7 @@ func suspendBgRefresh() {
|
||||
func terminateChat() {
|
||||
logger.debug("terminateChat")
|
||||
suspendLockQueue.sync {
|
||||
switch appStateGroupDefault.get() {
|
||||
switch AppChatState.shared.value {
|
||||
case .suspending:
|
||||
// suspend instantly if already suspending
|
||||
_chatSuspended()
|
||||
@@ -72,7 +70,7 @@ func terminateChat() {
|
||||
|
||||
func chatSuspended() {
|
||||
suspendLockQueue.sync {
|
||||
if case .suspending = appStateGroupDefault.get() {
|
||||
if case .suspending = AppChatState.shared.value {
|
||||
_chatSuspended()
|
||||
}
|
||||
}
|
||||
@@ -80,7 +78,7 @@ func chatSuspended() {
|
||||
|
||||
private func _chatSuspended() {
|
||||
logger.debug("_chatSuspended")
|
||||
appStateGroupDefault.set(.suspended)
|
||||
AppChatState.shared.set(.suspended)
|
||||
if ChatModel.shared.chatRunning == true {
|
||||
ChatReceiver.shared.stop()
|
||||
}
|
||||
@@ -89,14 +87,14 @@ private func _chatSuspended() {
|
||||
|
||||
func setAppState(_ appState: AppState) {
|
||||
suspendLockQueue.sync {
|
||||
appStateGroupDefault.set(appState)
|
||||
AppChatState.shared.set(appState)
|
||||
}
|
||||
}
|
||||
|
||||
func activateChat(appState: AppState = .active) {
|
||||
logger.debug("DEBUGGING: activateChat")
|
||||
suspendLockQueue.sync {
|
||||
appStateGroupDefault.set(appState)
|
||||
AppChatState.shared.set(appState)
|
||||
if ChatModel.ok { apiActivateChat() }
|
||||
logger.debug("DEBUGGING: activateChat: after apiActivateChat")
|
||||
}
|
||||
@@ -120,17 +118,22 @@ func startChatAndActivate(dispatchQueue: DispatchQueue = DispatchQueue.main, _ c
|
||||
ChatReceiver.shared.start()
|
||||
logger.debug("DEBUGGING: startChatAndActivate: after ChatReceiver.shared.start")
|
||||
}
|
||||
if .active == appStateGroupDefault.get() {
|
||||
if .active == AppChatState.shared.value {
|
||||
completion()
|
||||
} else if nseStateGroupDefault.get().inactive {
|
||||
activate()
|
||||
} else {
|
||||
suspendLockQueue.sync {
|
||||
appStateGroupDefault.set(.activating)
|
||||
}
|
||||
// TODO can be replaced with Mach messenger to notify the NSE to terminate and continue after reply, with timeout
|
||||
dispatchQueue.asyncAfter(deadline: .now() + activationDelay) {
|
||||
if appStateGroupDefault.get() == .activating {
|
||||
// setting app state to "activating" to notify NSE that it should suspend
|
||||
setAppState(.activating)
|
||||
waitNSESuspended(timeout: 10, dispatchQueue: dispatchQueue) { ok in
|
||||
if !ok {
|
||||
// if for some reason NSE failed to suspend,
|
||||
// e.g., it crashed previously without setting its state to "suspended",
|
||||
// set it to "suspended" state anyway, so that next time app
|
||||
// does not have to wait when activating.
|
||||
nseStateGroupDefault.set(.suspended)
|
||||
}
|
||||
if AppChatState.shared.value == .activating {
|
||||
activate()
|
||||
}
|
||||
}
|
||||
@@ -143,3 +146,19 @@ func startChatAndActivate(dispatchQueue: DispatchQueue = DispatchQueue.main, _ c
|
||||
logger.debug("DEBUGGING: startChatAndActivate: after activateChat")
|
||||
}
|
||||
}
|
||||
|
||||
// appStateGroupDefault must not be used in the app directly, only via this singleton
|
||||
class AppChatState {
|
||||
static let shared = AppChatState()
|
||||
private var value_ = appStateGroupDefault.get()
|
||||
|
||||
var value: AppState {
|
||||
value_
|
||||
}
|
||||
|
||||
func set(_ state: AppState) {
|
||||
appStateGroupDefault.set(state)
|
||||
sendAppState(state)
|
||||
value_ = state
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ struct SimpleXApp: App {
|
||||
NtfManager.shared.setNtfBadgeCount(chatModel.totalUnreadCountForAllUsers())
|
||||
case .active:
|
||||
CallController.shared.shouldSuspendChat = false
|
||||
let appState = appStateGroupDefault.get()
|
||||
let appState = AppChatState.shared.value
|
||||
startChatAndActivate {
|
||||
if appState.inactive && chatModel.chatRunning == true {
|
||||
updateChats()
|
||||
|
||||
@@ -415,7 +415,7 @@ struct DatabaseView: View {
|
||||
do {
|
||||
try initializeChat(start: true)
|
||||
m.chatDbChanged = false
|
||||
appStateGroupDefault.set(.active)
|
||||
AppChatState.shared.set(.active)
|
||||
} catch let error {
|
||||
fatalError("Error starting chat \(responseError(error))")
|
||||
}
|
||||
@@ -427,7 +427,7 @@ struct DatabaseView: View {
|
||||
m.chatRunning = true
|
||||
ChatReceiver.shared.start()
|
||||
chatLastStartGroupDefault.set(Date.now)
|
||||
appStateGroupDefault.set(.active)
|
||||
AppChatState.shared.set(.active)
|
||||
} catch let error {
|
||||
runChat = false
|
||||
alert = .error(title: "Error starting chat", error: responseError(error))
|
||||
@@ -477,7 +477,7 @@ func stopChatAsync() async throws {
|
||||
try await apiStopChat()
|
||||
ChatReceiver.shared.stop()
|
||||
await MainActor.run { ChatModel.shared.chatRunning = false }
|
||||
appStateGroupDefault.set(.stopped)
|
||||
AppChatState.shared.set(.stopped)
|
||||
}
|
||||
|
||||
func deleteChatAsync() async throws {
|
||||
|
||||
@@ -52,7 +52,7 @@ struct LocalAuthView: View {
|
||||
resetChatCtrl()
|
||||
try initializeChat(start: true)
|
||||
m.chatDbChanged = false
|
||||
appStateGroupDefault.set(.active)
|
||||
AppChatState.shared.set(.active)
|
||||
if m.currentUser != nil { return }
|
||||
var profile: Profile? = nil
|
||||
if let displayName = displayName, displayName != "" {
|
||||
|
||||
@@ -144,19 +144,37 @@ enum NSENotification {
|
||||
class NSEThreads {
|
||||
static let shared = NSEThreads()
|
||||
private static let queue = DispatchQueue(label: "chat.simplex.app.SimpleX-NSE.notification-threads.lock")
|
||||
private var threads: Set<UUID> = []
|
||||
private var allThreads: Set<UUID> = []
|
||||
private var activeThreads: Set<UUID> = []
|
||||
|
||||
func startThread() -> UUID {
|
||||
func newThread() -> UUID {
|
||||
NSEThreads.queue.sync {
|
||||
let (_, t) = threads.insert(UUID())
|
||||
let (_, t) = allThreads.insert(UUID())
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
func startThread(_ t: UUID) {
|
||||
NSEThreads.queue.sync {
|
||||
if allThreads.contains(t) {
|
||||
_ = activeThreads.insert(t)
|
||||
} else {
|
||||
logger.warning("NotificationService startThread: thread \(t) was removed before it started")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func endThread(_ t: UUID) -> Bool {
|
||||
NSEThreads.queue.sync {
|
||||
let t_ = threads.remove(t)
|
||||
return t_ != nil && threads.isEmpty
|
||||
let tActive = activeThreads.remove(t)
|
||||
let t = allThreads.remove(t)
|
||||
if tActive != nil && activeThreads.isEmpty {
|
||||
return true
|
||||
}
|
||||
if t != nil && allThreads.isEmpty {
|
||||
NSEChatState.shared.set(.suspended)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,16 +187,20 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
var contentHandler: ((UNNotificationContent) -> Void)?
|
||||
var bestAttemptNtf: NSENotification?
|
||||
var badgeCount: Int = 0
|
||||
// thread is added to allThreads here - if thread did not start chat,
|
||||
// chat does not need to be suspended but NSE state still needs to be set to "suspended".
|
||||
var threadId: UUID?
|
||||
var receiveEntityId: String?
|
||||
var cancelRead: (() -> Void)?
|
||||
var appSubscriber: AppSubscriber?
|
||||
var returnedSuspension = false
|
||||
|
||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
threadId = NSEThreads.shared.startThread()
|
||||
logger.debug("DEBUGGING: NotificationService.didReceive")
|
||||
if let ntf = request.content.mutableCopy() as? UNMutableNotificationContent {
|
||||
setBestAttemptNtf(ntf)
|
||||
}
|
||||
let newThreadId = NSEThreads.shared.newThread()
|
||||
threadId = newThreadId
|
||||
let ntf = if let ntf_ = request.content.mutableCopy() as? UNMutableNotificationContent { ntf_ } else { UNMutableNotificationContent() }
|
||||
setBestAttemptNtf(ntf)
|
||||
self.contentHandler = contentHandler
|
||||
registerGroupDefaults()
|
||||
let appState = appStateGroupDefault.get()
|
||||
@@ -186,20 +208,32 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
case .suspended:
|
||||
logger.debug("NotificationService: app is suspended")
|
||||
setBadgeCount()
|
||||
receiveNtfMessages(request, contentHandler)
|
||||
receiveNtfMessages(newThreadId, request, contentHandler)
|
||||
case .suspending:
|
||||
logger.debug("NotificationService: app is suspending")
|
||||
setBadgeCount()
|
||||
Task {
|
||||
var state = appState
|
||||
for _ in 1...6 {
|
||||
_ = try await Task.sleep(nanoseconds: suspendingDelay)
|
||||
state = appStateGroupDefault.get()
|
||||
if state == .suspended || state != .suspending { break }
|
||||
let state: AppState = await withCheckedContinuation { cont in
|
||||
appSubscriber = appStateSubscriber { s in
|
||||
if s == .suspended { appSuspension(s) }
|
||||
}
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + Double(appSuspendTimeout) + 1) {
|
||||
logger.debug("NotificationService: appSuspension timeout")
|
||||
appSuspension(appStateGroupDefault.get())
|
||||
}
|
||||
|
||||
@Sendable
|
||||
func appSuspension(_ s: AppState) {
|
||||
if !self.returnedSuspension {
|
||||
self.returnedSuspension = true
|
||||
self.appSubscriber = nil // this disposes of appStateSubscriber
|
||||
cont.resume(returning: s)
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.debug("NotificationService: app state is \(state.rawValue, privacy: .public)")
|
||||
if state.inactive {
|
||||
receiveNtfMessages(request, contentHandler)
|
||||
receiveNtfMessages(newThreadId, request, contentHandler)
|
||||
} else {
|
||||
deliverBestAttemptNtf()
|
||||
}
|
||||
@@ -210,7 +244,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
}
|
||||
}
|
||||
|
||||
func receiveNtfMessages(_ request: UNNotificationRequest, _ contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
func receiveNtfMessages(_ newThreadId: UUID, _ request: UNNotificationRequest, _ contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
logger.debug("NotificationService: receiveNtfMessages")
|
||||
if case .documents = dbContainerGroupDefault.get() {
|
||||
deliverBestAttemptNtf()
|
||||
@@ -220,7 +254,11 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
if let ntfData = userInfo["notificationData"] as? [AnyHashable : Any],
|
||||
let nonce = ntfData["nonce"] as? String,
|
||||
let encNtfInfo = ntfData["message"] as? String,
|
||||
let dbStatus = startChat() {
|
||||
// check it here again
|
||||
appStateGroupDefault.get().inactive {
|
||||
// thread is added to activeThreads tracking set here - if thread started chat it needs to be suspended
|
||||
NSEThreads.shared.startThread(newThreadId)
|
||||
let dbStatus = startChat()
|
||||
if case .ok = dbStatus,
|
||||
let ntfInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) {
|
||||
logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo), privacy: .public)")
|
||||
@@ -244,7 +282,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else if let dbStatus = dbStatus {
|
||||
setBestAttemptNtf(createErrorNtf(dbStatus))
|
||||
}
|
||||
}
|
||||
@@ -324,6 +362,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
}
|
||||
}
|
||||
|
||||
// nseStateGroupDefault must not be used in NSE directly, only via this singleton
|
||||
class NSEChatState {
|
||||
static let shared = NSEChatState()
|
||||
private var value_ = NSEState.created
|
||||
@@ -334,14 +373,34 @@ class NSEChatState {
|
||||
|
||||
func set(_ state: NSEState) {
|
||||
nseStateGroupDefault.set(state)
|
||||
sendNSEState(state)
|
||||
value_ = state
|
||||
}
|
||||
|
||||
init() {
|
||||
// This is always set to .created state, as in case previous start of NSE crashed in .active state, it is stored correctly.
|
||||
// Otherwise the app will be activating slower
|
||||
set(.created)
|
||||
}
|
||||
}
|
||||
|
||||
var appSubscriber: AppSubscriber = appStateSubscriber { state in
|
||||
logger.debug("NotificationService: appSubscriber")
|
||||
if state.running && NSEChatState.shared.value.canSuspend {
|
||||
logger.debug("NotificationService: appSubscriber app state \(state.rawValue), suspending")
|
||||
suspendChat(nseSuspendTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
func appStateSubscriber(onState: @escaping (AppState) -> Void) -> AppSubscriber {
|
||||
appMessageSubscriber { msg in
|
||||
if case let .state(state) = msg {
|
||||
logger.debug("NotificationService: appStateSubscriber \(state.rawValue, privacy: .public)")
|
||||
onState(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var receiverStarted = false
|
||||
let startLock = DispatchSemaphore(value: 1)
|
||||
let suspendLock = DispatchSemaphore(value: 1)
|
||||
@@ -359,6 +418,7 @@ func startChat() -> DBMigrationResult? {
|
||||
|
||||
return switch NSEChatState.shared.value {
|
||||
case .created: doStartChat()
|
||||
case .starting: .ok // it should never get to this branch, as it would be waiting for start on startLock
|
||||
case .active: .ok
|
||||
case .suspending: activateChat()
|
||||
case .suspended: activateChat()
|
||||
@@ -374,6 +434,8 @@ func doStartChat() -> DBMigrationResult? {
|
||||
NSEChatState.shared.set(.created)
|
||||
return dbStatus
|
||||
}
|
||||
let state = NSEChatState.shared.value
|
||||
NSEChatState.shared.set(.starting)
|
||||
if let user = apiGetActiveUser() {
|
||||
logger.debug("NotificationService active user \(String(describing: user))")
|
||||
do {
|
||||
@@ -382,24 +444,31 @@ func doStartChat() -> DBMigrationResult? {
|
||||
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
||||
try setXFTPConfig(xftpConfig)
|
||||
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
|
||||
let justStarted = try apiStartChat()
|
||||
NSEChatState.shared.set(.active)
|
||||
if justStarted {
|
||||
chatLastStartGroupDefault.set(Date.now)
|
||||
Task {
|
||||
if !receiverStarted {
|
||||
receiverStarted = true
|
||||
await receiveMessages()
|
||||
// prevent suspension while starting chat
|
||||
suspendLock.wait()
|
||||
defer { suspendLock.signal() }
|
||||
if NSEChatState.shared.value == .starting {
|
||||
updateNetCfg()
|
||||
let justStarted = try apiStartChat()
|
||||
NSEChatState.shared.set(.active)
|
||||
if justStarted {
|
||||
chatLastStartGroupDefault.set(Date.now)
|
||||
Task {
|
||||
if !receiverStarted {
|
||||
receiverStarted = true
|
||||
await receiveMessages()
|
||||
}
|
||||
}
|
||||
}
|
||||
return .ok
|
||||
}
|
||||
return .ok
|
||||
} catch {
|
||||
logger.error("NotificationService startChat error: \(responseError(error), privacy: .public)")
|
||||
}
|
||||
} else {
|
||||
logger.debug("NotificationService: no active user")
|
||||
}
|
||||
if NSEChatState.shared.value == .starting { NSEChatState.shared.set(state) }
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -450,15 +519,10 @@ func receiveMessages() async {
|
||||
logger.debug("NotificationService receiveMessages")
|
||||
while true {
|
||||
switch NSEChatState.shared.value {
|
||||
// it should never get to "created" and "starting" branches, as NSE state is set to .active before the loop start
|
||||
case .created: await delayWhenInactive()
|
||||
case .active:
|
||||
if appStateGroupDefault.get().running {
|
||||
suspendChat(nseSuspendTimeout)
|
||||
await delayWhenInactive()
|
||||
} else {
|
||||
updateNetCfg()
|
||||
await receiveMsg()
|
||||
}
|
||||
case .starting: await delayWhenInactive()
|
||||
case .active: await receiveMsg()
|
||||
case .suspending: await receiveMsg()
|
||||
case .suspended: await delayWhenInactive()
|
||||
}
|
||||
|
||||
@@ -43,11 +43,6 @@
|
||||
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */; };
|
||||
5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */; };
|
||||
5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B3B09285FB130003915F2 /* DatabaseView.swift */; };
|
||||
5C4BB4CC2B20E177007981AA /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4BB4C72B20E176007981AA /* libffi.a */; };
|
||||
5C4BB4CD2B20E177007981AA /* libHSsimplex-chat-5.4.0.6-CwRTIkaIEVTLXC76NTPbOy.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4BB4C82B20E176007981AA /* libHSsimplex-chat-5.4.0.6-CwRTIkaIEVTLXC76NTPbOy.a */; };
|
||||
5C4BB4CE2B20E177007981AA /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4BB4C92B20E177007981AA /* libgmpxx.a */; };
|
||||
5C4BB4CF2B20E177007981AA /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4BB4CA2B20E177007981AA /* libgmp.a */; };
|
||||
5C4BB4D02B20E177007981AA /* libHSsimplex-chat-5.4.0.6-CwRTIkaIEVTLXC76NTPbOy-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4BB4CB2B20E177007981AA /* libHSsimplex-chat-5.4.0.6-CwRTIkaIEVTLXC76NTPbOy-ghc9.6.3.a */; };
|
||||
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; };
|
||||
5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A91E283AD0E400C4E99E /* CallManager.swift */; };
|
||||
5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */; };
|
||||
@@ -68,6 +63,11 @@
|
||||
5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; };
|
||||
5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */; };
|
||||
5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E88279CBCB3000C6508 /* ChatModel.swift */; };
|
||||
5C8EA13D2B25206A001DE5E4 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C8EA1382B25206A001DE5E4 /* libgmp.a */; };
|
||||
5C8EA13E2B25206A001DE5E4 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C8EA1392B25206A001DE5E4 /* libgmpxx.a */; };
|
||||
5C8EA13F2B25206A001DE5E4 /* libHSsimplex-chat-5.4.0.7-1uCDT6bmj7t4ctyD1vFaZX-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C8EA13A2B25206A001DE5E4 /* libHSsimplex-chat-5.4.0.7-1uCDT6bmj7t4ctyD1vFaZX-ghc9.6.3.a */; };
|
||||
5C8EA1402B25206A001DE5E4 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C8EA13B2B25206A001DE5E4 /* libffi.a */; };
|
||||
5C8EA1412B25206A001DE5E4 /* libHSsimplex-chat-5.4.0.7-1uCDT6bmj7t4ctyD1vFaZX.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C8EA13C2B25206A001DE5E4 /* libHSsimplex-chat-5.4.0.7-1uCDT6bmj7t4ctyD1vFaZX.a */; };
|
||||
5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 5C8F01CC27A6F0D8007D2C8D /* CodeScanner */; };
|
||||
5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */; };
|
||||
5C93293129239BED0090FFF9 /* ProtocolServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */; };
|
||||
@@ -151,6 +151,8 @@
|
||||
5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */; };
|
||||
5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */; };
|
||||
5CF9371E2B23429500E1D781 /* ConcurrentQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF9371D2B23429500E1D781 /* ConcurrentQueue.swift */; };
|
||||
5CF937202B24DE8C00E1D781 /* SharedFileSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */; };
|
||||
5CF937232B2503D000E1D781 /* NSESubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF937212B25034A00E1D781 /* NSESubscriber.swift */; };
|
||||
5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; };
|
||||
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */; };
|
||||
5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
|
||||
@@ -291,11 +293,6 @@
|
||||
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>"; };
|
||||
5C4B3B09285FB130003915F2 /* DatabaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseView.swift; sourceTree = "<group>"; };
|
||||
5C4BB4C72B20E176007981AA /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5C4BB4C82B20E176007981AA /* libHSsimplex-chat-5.4.0.6-CwRTIkaIEVTLXC76NTPbOy.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.6-CwRTIkaIEVTLXC76NTPbOy.a"; sourceTree = "<group>"; };
|
||||
5C4BB4C92B20E177007981AA /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5C4BB4CA2B20E177007981AA /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5C4BB4CB2B20E177007981AA /* libHSsimplex-chat-5.4.0.6-CwRTIkaIEVTLXC76NTPbOy-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.6-CwRTIkaIEVTLXC76NTPbOy-ghc9.6.3.a"; 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>"; };
|
||||
5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = "<group>"; };
|
||||
@@ -336,6 +333,11 @@
|
||||
5C8B41C929AF41BC00888272 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
5C8B41CB29AF44CF00888272 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = "cs.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
5C8B41CC29AF44CF00888272 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
5C8EA1382B25206A001DE5E4 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5C8EA1392B25206A001DE5E4 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5C8EA13A2B25206A001DE5E4 /* libHSsimplex-chat-5.4.0.7-1uCDT6bmj7t4ctyD1vFaZX-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.7-1uCDT6bmj7t4ctyD1vFaZX-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
5C8EA13B2B25206A001DE5E4 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5C8EA13C2B25206A001DE5E4 /* libHSsimplex-chat-5.4.0.7-1uCDT6bmj7t4ctyD1vFaZX.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.7-1uCDT6bmj7t4ctyD1vFaZX.a"; sourceTree = "<group>"; };
|
||||
5C93292E29239A170090FFF9 /* ProtocolServersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolServersView.swift; sourceTree = "<group>"; };
|
||||
5C93293029239BED0090FFF9 /* ProtocolServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolServerView.swift; sourceTree = "<group>"; };
|
||||
5C93293E2928E0FD0090FFF9 /* AudioRecPlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecPlay.swift; sourceTree = "<group>"; };
|
||||
@@ -436,6 +438,8 @@
|
||||
5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = "<group>"; };
|
||||
5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDeliveryReceiptsView.swift; sourceTree = "<group>"; };
|
||||
5CF9371D2B23429500E1D781 /* ConcurrentQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrentQueue.swift; sourceTree = "<group>"; };
|
||||
5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedFileSubscriber.swift; sourceTree = "<group>"; };
|
||||
5CF937212B25034A00E1D781 /* NSESubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSESubscriber.swift; sourceTree = "<group>"; };
|
||||
5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = "<group>"; };
|
||||
5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = "<group>"; };
|
||||
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; };
|
||||
@@ -513,12 +517,12 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5C4BB4D02B20E177007981AA /* libHSsimplex-chat-5.4.0.6-CwRTIkaIEVTLXC76NTPbOy-ghc9.6.3.a in Frameworks */,
|
||||
5C8EA13F2B25206A001DE5E4 /* libHSsimplex-chat-5.4.0.7-1uCDT6bmj7t4ctyD1vFaZX-ghc9.6.3.a in Frameworks */,
|
||||
5C8EA1402B25206A001DE5E4 /* libffi.a in Frameworks */,
|
||||
5C8EA13E2B25206A001DE5E4 /* libgmpxx.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
5C4BB4CE2B20E177007981AA /* libgmpxx.a in Frameworks */,
|
||||
5C4BB4CC2B20E177007981AA /* libffi.a in Frameworks */,
|
||||
5C4BB4CD2B20E177007981AA /* libHSsimplex-chat-5.4.0.6-CwRTIkaIEVTLXC76NTPbOy.a in Frameworks */,
|
||||
5C4BB4CF2B20E177007981AA /* libgmp.a in Frameworks */,
|
||||
5C8EA13D2B25206A001DE5E4 /* libgmp.a in Frameworks */,
|
||||
5C8EA1412B25206A001DE5E4 /* libHSsimplex-chat-5.4.0.7-1uCDT6bmj7t4ctyD1vFaZX.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -581,11 +585,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5C4BB4C72B20E176007981AA /* libffi.a */,
|
||||
5C4BB4CA2B20E177007981AA /* libgmp.a */,
|
||||
5C4BB4C92B20E177007981AA /* libgmpxx.a */,
|
||||
5C4BB4CB2B20E177007981AA /* libHSsimplex-chat-5.4.0.6-CwRTIkaIEVTLXC76NTPbOy-ghc9.6.3.a */,
|
||||
5C4BB4C82B20E176007981AA /* libHSsimplex-chat-5.4.0.6-CwRTIkaIEVTLXC76NTPbOy.a */,
|
||||
5C8EA13B2B25206A001DE5E4 /* libffi.a */,
|
||||
5C8EA1382B25206A001DE5E4 /* libgmp.a */,
|
||||
5C8EA1392B25206A001DE5E4 /* libgmpxx.a */,
|
||||
5C8EA13A2B25206A001DE5E4 /* libHSsimplex-chat-5.4.0.7-1uCDT6bmj7t4ctyD1vFaZX-ghc9.6.3.a */,
|
||||
5C8EA13C2B25206A001DE5E4 /* libHSsimplex-chat-5.4.0.7-1uCDT6bmj7t4ctyD1vFaZX.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -610,6 +614,7 @@
|
||||
5C35CFC727B2782E00FB6C6D /* BGManager.swift */,
|
||||
5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */,
|
||||
5CB346E42868AA7F001FD2EF /* SuspendChat.swift */,
|
||||
5CF937212B25034A00E1D781 /* NSESubscriber.swift */,
|
||||
5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */,
|
||||
5C93293E2928E0FD0090FFF9 /* AudioRecPlay.swift */,
|
||||
5CBD2859295711D700EC2CF4 /* ImageUtils.swift */,
|
||||
@@ -811,6 +816,7 @@
|
||||
64DAE1502809D9F5000DA960 /* FileUtils.swift */,
|
||||
5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */,
|
||||
5C00168028C4FE760094D739 /* KeyChain.swift */,
|
||||
5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */,
|
||||
5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */,
|
||||
5CE2BA8A2845332200EC33A6 /* SimpleX.h */,
|
||||
5CE2BA78284530CC00EC33A6 /* SimpleXChat.docc */,
|
||||
@@ -1183,6 +1189,7 @@
|
||||
5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */,
|
||||
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */,
|
||||
6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */,
|
||||
5CF937232B2503D000E1D781 /* NSESubscriber.swift in Sources */,
|
||||
6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */,
|
||||
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */,
|
||||
64466DCC29FFE3E800E3D48D /* MailView.swift in Sources */,
|
||||
@@ -1270,6 +1277,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5CF937202B24DE8C00E1D781 /* SharedFileSubscriber.swift in Sources */,
|
||||
5C00168128C4FE760094D739 /* KeyChain.swift in Sources */,
|
||||
5CE2BA97284537A800EC33A6 /* dummy.m in Sources */,
|
||||
5CE2BA922845340900EC33A6 /* FileUtils.swift in Sources */,
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
public let appSuspendTimeout: Int = 15 // seconds
|
||||
|
||||
let GROUP_DEFAULT_APP_STATE = "appState"
|
||||
let GROUP_DEFAULT_NSE_STATE = "nseState"
|
||||
let GROUP_DEFAULT_DB_CONTAINER = "dbContainer"
|
||||
@@ -67,7 +69,7 @@ public func registerGroupDefaults() {
|
||||
])
|
||||
}
|
||||
|
||||
public enum AppState: String {
|
||||
public enum AppState: String, Codable {
|
||||
case active
|
||||
case activating
|
||||
case bgRefresh
|
||||
@@ -102,8 +104,9 @@ public enum AppState: String {
|
||||
}
|
||||
}
|
||||
|
||||
public enum NSEState: String {
|
||||
public enum NSEState: String, Codable {
|
||||
case created
|
||||
case starting
|
||||
case active
|
||||
case suspending
|
||||
case suspended
|
||||
@@ -126,16 +129,18 @@ public enum DBContainer: String {
|
||||
case group
|
||||
}
|
||||
|
||||
// appStateGroupDefault must not be used in the app directly, only via AppChatState singleton
|
||||
public let appStateGroupDefault = EnumDefault<AppState>(
|
||||
defaults: groupDefaults,
|
||||
forKey: GROUP_DEFAULT_APP_STATE,
|
||||
withDefault: .active
|
||||
)
|
||||
|
||||
// nseStateGroupDefault must not be used in NSE directly, only via NSEChatState singleton
|
||||
public let nseStateGroupDefault = EnumDefault<NSEState>(
|
||||
defaults: groupDefaults,
|
||||
forKey: GROUP_DEFAULT_NSE_STATE,
|
||||
withDefault: .created
|
||||
withDefault: .suspended // so that NSE that was never launched does not delay the app from resuming
|
||||
)
|
||||
|
||||
public func allowBackgroundRefresh() -> Bool {
|
||||
|
||||
99
apps/ios/SimpleXChat/SharedFileSubscriber.swift
Normal file
99
apps/ios/SimpleXChat/SharedFileSubscriber.swift
Normal file
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// SharedFileSubscriber.swift
|
||||
// SimpleXChat
|
||||
//
|
||||
// Created by Evgeny on 09/12/2023.
|
||||
// Copyright © 2023 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public typealias AppSubscriber = SharedFileSubscriber<ProcessMessage<AppProcessMessage>>
|
||||
|
||||
public typealias NSESubscriber = SharedFileSubscriber<ProcessMessage<NSEProcessMessage>>
|
||||
|
||||
public class SharedFileSubscriber<Message: Codable>: NSObject, NSFilePresenter {
|
||||
var fileURL: URL
|
||||
public var presentedItemURL: URL?
|
||||
public var presentedItemOperationQueue: OperationQueue = .main
|
||||
var subscriber: (Message) -> Void
|
||||
|
||||
init(fileURL: URL, onMessage: @escaping (Message) -> Void) {
|
||||
self.fileURL = fileURL
|
||||
presentedItemURL = fileURL
|
||||
subscriber = onMessage
|
||||
super.init()
|
||||
NSFileCoordinator.addFilePresenter(self)
|
||||
}
|
||||
|
||||
public func presentedItemDidChange() {
|
||||
do {
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
let msg = try jsonDecoder.decode(Message.self, from: data)
|
||||
subscriber(msg)
|
||||
} catch let error {
|
||||
logger.error("presentedItemDidChange error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
static func notify(url: URL, message: Message) {
|
||||
let fc = NSFileCoordinator(filePresenter: nil)
|
||||
fc.coordinate(writingItemAt: url, options: [], error: nil) { newURL in
|
||||
do {
|
||||
let data = try jsonEncoder.encode(message)
|
||||
try data.write(to: newURL, options: [.atomic])
|
||||
} catch {
|
||||
logger.error("notifyViaSharedFile error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
NSFileCoordinator.removeFilePresenter(self)
|
||||
}
|
||||
}
|
||||
|
||||
let appMessagesSharedFile = getGroupContainerDirectory().appendingPathComponent("chat.simplex.app.messages", isDirectory: false)
|
||||
|
||||
let nseMessagesSharedFile = getGroupContainerDirectory().appendingPathComponent("chat.simplex.app.SimpleX-NSE.messages", isDirectory: false)
|
||||
|
||||
public struct ProcessMessage<Message: Codable>: Codable {
|
||||
var createdAt: Date = Date.now
|
||||
var message: Message
|
||||
}
|
||||
|
||||
public enum AppProcessMessage: Codable {
|
||||
case state(state: AppState)
|
||||
}
|
||||
|
||||
public enum NSEProcessMessage: Codable {
|
||||
case state(state: NSEState)
|
||||
}
|
||||
|
||||
public func sendAppProcessMessage(_ message: AppProcessMessage) {
|
||||
SharedFileSubscriber.notify(url: appMessagesSharedFile, message: ProcessMessage(message: message))
|
||||
}
|
||||
|
||||
public func sendNSEProcessMessage(_ message: NSEProcessMessage) {
|
||||
SharedFileSubscriber.notify(url: nseMessagesSharedFile, message: ProcessMessage(message: message))
|
||||
}
|
||||
|
||||
public func appMessageSubscriber(onMessage: @escaping (AppProcessMessage) -> Void) -> AppSubscriber {
|
||||
SharedFileSubscriber(fileURL: appMessagesSharedFile) { (msg: ProcessMessage<AppProcessMessage>) in
|
||||
onMessage(msg.message)
|
||||
}
|
||||
}
|
||||
|
||||
public func nseMessageSubscriber(onMessage: @escaping (NSEProcessMessage) -> Void) -> NSESubscriber {
|
||||
SharedFileSubscriber(fileURL: nseMessagesSharedFile) { (msg: ProcessMessage<NSEProcessMessage>) in
|
||||
onMessage(msg.message)
|
||||
}
|
||||
}
|
||||
|
||||
public func sendAppState(_ state: AppState) {
|
||||
sendAppProcessMessage(.state(state: state))
|
||||
}
|
||||
|
||||
public func sendNSEState(_ state: NSEState) {
|
||||
sendNSEProcessMessage(.state(state: state))
|
||||
}
|
||||
Reference in New Issue
Block a user