diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index a66c3e503a..0392e7274b 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -9,6 +9,7 @@ import SwiftUI struct ContentView: View { @EnvironmentObject var chatModel: ChatModel + @State private var showNotificationAlert = false var body: some View { if let user = chatModel.currentUser { @@ -20,21 +21,27 @@ struct ContentView: View { } catch { fatalError("Failed to start or load chats: \(error)") } - - DispatchQueue.global().async { - while(true) { - do { - try processReceivedMsg(chatModel, chatRecvMsg()) - } catch { - print("error receiving message: ", error) - } - } - } + ChatReceiver.shared.start() + NtfManager.shared.requestAuthorization(onDeny: { + showNotificationAlert = true + }) + } + .alert(isPresented : $showNotificationAlert){ + Alert( + title: Text("Notification are disabled!"), + message: Text("Please open settings to enable"), + primaryButton: .default(Text("Open Settings")) { + DispatchQueue.main.async { + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) + } + }, + secondaryButton: .cancel() + ) } } else { WelcomeView() } - } + } } diff --git a/apps/ios/Shared/Model/BGManager.swift b/apps/ios/Shared/Model/BGManager.swift new file mode 100644 index 0000000000..9365d42777 --- /dev/null +++ b/apps/ios/Shared/Model/BGManager.swift @@ -0,0 +1,97 @@ +// +// BGManager.swift +// SimpleX +// +// Created by Evgeny Poberezkin on 08/02/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import Foundation +import BackgroundTasks + +private let receiveTaskId = "chat.simplex.app.receive" + +// TCP timeout + 2 sec +private let waitForMessages: TimeInterval = 6 + +class BGManager { + private var bgTimer: Timer? + + static let shared = BGManager() + + func register() { + logger.debug("BGManager.register") + BGTaskScheduler.shared.register(forTaskWithIdentifier: receiveTaskId, using: nil) { task in + self.handleRefresh(RefreshTask(task as! BGAppRefreshTask)) + } + } + + func schedule() { + logger.debug("BGManager.schedule") + let request = BGAppRefreshTaskRequest(identifier: receiveTaskId) + request.earliestBeginDate = Date(timeIntervalSinceNow: 10 * 60) + do { + try BGTaskScheduler.shared.submit(request) + } catch { + logger.error("BGManager.schedule error: \(error.localizedDescription)") + } + } + + private func handleRefresh(_ task: RefreshTask) { + logger.debug("BGManager.handleRefresh") + schedule() + task.expirationHandler = { + logger.debug("BGManager.handleRefresh expirationHandler") + ChatReceiver.shared.stop() + task.setTaskCompleted(success: true) + } + DispatchQueue.main.async { + initializeChat() + if ChatModel.shared.currentUser == nil { + task.setTaskCompleted(success: true) + return + } + logger.debug("BGManager.handleRefresh: starting chat") + ChatReceiver.shared.start(bgTask: task) + RunLoop.current.add(Timer(timeInterval: 2, repeats: true) { timer in + self.bgTimer = timer + logger.debug("BGManager.handleRefresh: timer") + if ChatReceiver.shared.lastMsgTime.distance(to: Date.now) >= waitForMessages { + logger.debug("BGManager.handleRefresh: timer: stopping") + ChatReceiver.shared.stop() + task.setTaskCompleted(success: true) + timer.invalidate() + self.bgTimer = nil + } + }, forMode: .default) + } + } + + func invalidateStopTimer() { + logger.debug("BGManager.invalidateStopTimer?") + if let timer = bgTimer { + timer.invalidate() + bgTimer = nil + logger.debug("BGManager.invalidateStopTimer: done") + } + } + + class RefreshTask { + private let task: BGAppRefreshTask + var completed = false + + internal init(_ task: BGAppRefreshTask) { + self.task = task + } + + var expirationHandler: (() -> Void)? { + set { task.expirationHandler = newValue } + get { task.expirationHandler } + } + + func setTaskCompleted(success: Bool) { + if !completed { task.setTaskCompleted(success: success) } + completed = true + } + } +} diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 34e5fa5bb8..db216b2fe9 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -22,6 +22,7 @@ final class ChatModel: ObservableObject { @Published var userAddress: String? @Published var appOpenUrl: URL? @Published var connectViaUrl = false + static let shared = ChatModel() func hasChat(_ id: String) -> Bool { chats.first(where: { $0.id == id }) != nil @@ -84,8 +85,6 @@ final class ChatModel: ObservableObject { } if chatId == cInfo.id { withAnimation { chatItems.append(cItem) } - } else if chatId != nil { - // meesage arrived to some other chat, show notification } } @@ -129,7 +128,7 @@ typealias ContactName = String typealias GroupName = String -struct Profile: Codable { +struct Profile: Codable, NamedChat { var displayName: String var fullName: String @@ -145,7 +144,20 @@ enum ChatType: String { case contactRequest = "<@" } -enum ChatInfo: Identifiable, Decodable { +protocol NamedChat { + var displayName: String { get } + var fullName: String { get } +} + +extension NamedChat { + var chatViewName: String { + get { displayName + (fullName == "" || fullName == displayName ? "" : " / \(fullName)") } + } +} + +typealias ChatId = String + +enum ChatInfo: Identifiable, Decodable, NamedChat { case direct(contact: Contact) case group(groupInfo: GroupInfo) case contactRequest(contactRequest: UserContactRequest) @@ -163,9 +175,9 @@ enum ChatInfo: Identifiable, Decodable { var displayName: String { get { switch self { - case let .direct(contact): return contact.profile.displayName - case let .group(groupInfo): return groupInfo.groupProfile.displayName - case let .contactRequest(contactRequest): return contactRequest.profile.displayName + case let .direct(contact): return contact.displayName + case let .group(groupInfo): return groupInfo.displayName + case let .contactRequest(contactRequest): return contactRequest.displayName } } } @@ -173,18 +185,14 @@ enum ChatInfo: Identifiable, Decodable { var fullName: String { get { switch self { - case let .direct(contact): return contact.profile.fullName - case let .group(groupInfo): return groupInfo.groupProfile.fullName - case let .contactRequest(contactRequest): return contactRequest.profile.fullName + case let .direct(contact): return contact.fullName + case let .group(groupInfo): return groupInfo.fullName + case let .contactRequest(contactRequest): return contactRequest.fullName } } } - var chatViewName: String { - get { displayName + (fullName == "" || fullName == displayName ? "" : " / \(fullName)") } - } - - var id: String { + var id: ChatId { get { switch self { case let .direct(contact): return contact.id @@ -292,17 +300,17 @@ final class Chat: ObservableObject, Identifiable { self.chatItems = chatItems } - var id: String { get { chatInfo.id } } + var id: ChatId { get { chatInfo.id } } } struct ChatData: Decodable, Identifiable { var chatInfo: ChatInfo var chatItems: [ChatItem] - var id: String { get { chatInfo.id } } + var id: ChatId { get { chatInfo.id } } } -struct Contact: Identifiable, Decodable { +struct Contact: Identifiable, Decodable, NamedChat { var contactId: Int64 var localDisplayName: ContactName var profile: Profile @@ -310,9 +318,11 @@ struct Contact: Identifiable, Decodable { var viaGroup: Int64? var createdAt: Date - var id: String { get { "@\(contactId)" } } + var id: ChatId { get { "@\(contactId)" } } var apiId: Int64 { get { contactId } } var ready: Bool { get { activeConn.connStatus == "ready" || activeConn.connStatus == "snd-ready" } } + var displayName: String { get { profile.displayName } } + var fullName: String { get { profile.fullName } } static let sampleData = Contact( contactId: 1, @@ -329,15 +339,16 @@ struct Connection: Decodable { static let sampleData = Connection(connStatus: "ready") } -struct UserContactRequest: Decodable { +struct UserContactRequest: Decodable, NamedChat { var contactRequestId: Int64 var localDisplayName: ContactName var profile: Profile var createdAt: Date - var id: String { get { "<@\(contactRequestId)" } } - + var id: ChatId { get { "<@\(contactRequestId)" } } var apiId: Int64 { get { contactRequestId } } + var displayName: String { get { profile.displayName } } + var fullName: String { get { profile.fullName } } static let sampleData = UserContactRequest( contactRequestId: 1, @@ -347,15 +358,16 @@ struct UserContactRequest: Decodable { ) } -struct GroupInfo: Identifiable, Decodable { +struct GroupInfo: Identifiable, Decodable, NamedChat { var groupId: Int64 var localDisplayName: GroupName var groupProfile: GroupProfile var createdAt: Date - var id: String { get { "#\(groupId)" } } - + var id: ChatId { get { "#\(groupId)" } } var apiId: Int64 { get { groupId } } + var displayName: String { get { groupProfile.displayName } } + var fullName: String { get { groupProfile.fullName } } static let sampleData = GroupInfo( groupId: 1, @@ -365,7 +377,7 @@ struct GroupInfo: Identifiable, Decodable { ) } -struct GroupProfile: Codable { +struct GroupProfile: Codable, NamedChat { var displayName: String var fullName: String diff --git a/apps/ios/Shared/Model/NtfManager.swift b/apps/ios/Shared/Model/NtfManager.swift new file mode 100644 index 0000000000..904357eec6 --- /dev/null +++ b/apps/ios/Shared/Model/NtfManager.swift @@ -0,0 +1,199 @@ +// +// NtfManager.swift +// SimpleX +// +// Created by Evgeny Poberezkin on 08/02/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import Foundation +import UserNotifications +import UIKit + +let ntfActionAccept = "NTF_ACT_ACCEPT" + +let ntfCategoryContactRequest = "NTF_CAT_CONTACT_REQUEST" +let ntfCategoryContactConnected = "NTF_CAT_CONTACT_CONNECTED" +let ntfCategoryMessageReceived = "NTF_CAT_MESSAGE_RECEIVED" + +let appNotificationId = "chat.simplex.app.notification" + +private let ntfTimeInterval: TimeInterval = 1 + +class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { + static let shared = NtfManager() + + private var granted = false + private var prevNtfTime: Dictionary = [:] + + + // Handle notification when app is in background + func userNotificationCenter(_ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler handler: () -> Void) { + logger.debug("NtfManager.userNotificationCenter: didReceive") + let content = response.notification.request.content + let chatModel = ChatModel.shared + if content.categoryIdentifier == ntfCategoryContactRequest && response.actionIdentifier == ntfActionAccept, + let chatId = content.userInfo["chatId"] as? String, + case let .contactRequest(contactRequest) = chatModel.getChat(chatId)?.chatInfo { + acceptContactRequest(contactRequest) + } else { + chatModel.chatId = content.targetContentIdentifier + } + handler() + } + + // Handle notification when the app is in foreground + func userNotificationCenter(_ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler handler: (UNNotificationPresentationOptions) -> Void) { + logger.debug("NtfManager.userNotificationCenter: willPresent") + handler(presentationOptions(notification.request.content)) + } + + private func presentationOptions(_ content: UNNotificationContent) -> UNNotificationPresentationOptions { + let model = ChatModel.shared + if UIApplication.shared.applicationState == .active { + switch content.categoryIdentifier { + case ntfCategoryContactRequest: + return [.sound, .banner, .list] + case ntfCategoryContactConnected: + return model.chatId == nil ? [.sound, .list] : [.sound, .banner, .list] + case ntfCategoryMessageReceived: + if model.chatId == nil { + // in the chat list + return recentInTheSameChat(content) ? [] : [.sound, .list] + } else if model.chatId == content.targetContentIdentifier { + // in the current chat + return recentInTheSameChat(content) ? [] : [.sound, .list] + } else { + // in another chat + return recentInTheSameChat(content) ? [.banner, .list] : [.sound, .banner, .list] + } + default: return [.sound, .banner, .list] + } + } else { + return [.sound, .banner, .list] + } + } + + private func recentInTheSameChat(_ content: UNNotificationContent) -> Bool { + let now = Date.now + if let chatId = content.targetContentIdentifier { + var res: Bool = false + if let t = prevNtfTime[chatId] { res = t.distance(to: now) < 30 } + prevNtfTime[chatId] = now + return res + } + return false + } + + func registerCategories() { + logger.debug("NtfManager.registerCategories") + UNUserNotificationCenter.current().setNotificationCategories([ + UNNotificationCategory( + identifier: ntfCategoryContactRequest, + actions: [UNNotificationAction( + identifier: ntfActionAccept, + title: "Accept" + )], + intentIdentifiers: [], + hiddenPreviewsBodyPlaceholder: "New contact request" + ), + UNNotificationCategory( + identifier: ntfCategoryContactConnected, + actions: [], + intentIdentifiers: [], + hiddenPreviewsBodyPlaceholder: "Contact is connected" + ), + UNNotificationCategory( + identifier: ntfCategoryMessageReceived, + actions: [], + intentIdentifiers: [], + hiddenPreviewsBodyPlaceholder: "New message" + ) + ]) + } + + func requestAuthorization(onDeny handler: (()-> Void)? = nil) { + logger.debug("NtfManager.requestAuthorization") + let center = UNUserNotificationCenter.current() + center.getNotificationSettings { settings in + switch settings.authorizationStatus { + case .denied: + if let handler = handler { handler() } + return + case .authorized: + self.granted = true + default: + center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in + if let error = error { + logger.error("NtfManager.requestAuthorization error \(error.localizedDescription)") + } else { + self.granted = granted + } + } + } + } + center.delegate = self + } + + func notifyContactRequest(_ contactRequest: UserContactRequest) { + logger.debug("NtfManager.notifyContactRequest") + addNotification( + categoryIdentifier: ntfCategoryContactRequest, + title: "\(contactRequest.displayName) wants to connect!", + body: "Accept contact request from \(contactRequest.chatViewName)?", + targetContentIdentifier: nil, + userInfo: ["chatId": contactRequest.id, "contactRequestId": contactRequest.apiId] + ) + } + + func notifyContactConnected(_ contact: Contact) { + logger.debug("NtfManager.notifyContactConnected") + addNotification( + categoryIdentifier: ntfCategoryContactConnected, + title: "\(contact.displayName) is connected!", + body: "You can now send messages to \(contact.chatViewName)", + targetContentIdentifier: contact.id +// userInfo: ["chatId": contact.id, "contactId": contact.apiId] + ) + } + + func notifyMessageReceived(_ cInfo: ChatInfo, _ cItem: ChatItem) { + logger.debug("NtfManager.notifyMessageReceived") + addNotification( + categoryIdentifier: ntfCategoryMessageReceived, + title: "\(cInfo.chatViewName):", + body: cItem.content.text, + targetContentIdentifier: cInfo.id +// userInfo: ["chatId": cInfo.id, "chatItemId": cItem.id] + ) + } + + private func addNotification(categoryIdentifier: String, title: String, subtitle: String? = nil, body: String? = nil, + targetContentIdentifier: String? = nil, userInfo: [AnyHashable : Any] = [:]) { + if !granted { return } + let content = UNMutableNotificationContent() + content.categoryIdentifier = categoryIdentifier + content.title = title + if let s = subtitle { content.subtitle = s } + if let s = body { content.body = s } + content.targetContentIdentifier = targetContentIdentifier + content.userInfo = userInfo + content.sound = .default +// content.interruptionLevel = .active +// content.relevanceScore = 0.5 // 0-1 + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: ntfTimeInterval, repeats: false) + let request = UNNotificationRequest(identifier: appNotificationId, content: content, trigger: trigger) + UNUserNotificationCenter.current().add(request) { error in + if let error = error { logger.error("addNotification error: \(error.localizedDescription)") } + } + } + + func removeNotifications(_ ids : [String]){ + UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: ids) + UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ids) + } +} diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 46e2a434f6..00d604ccbf 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -8,6 +8,8 @@ import Foundation import UIKit +import Dispatch +import BackgroundTasks private var chatController: chat_ctrl? private let jsonDecoder = getJSONDecoder() @@ -82,6 +84,9 @@ enum ChatResponse: Decodable, Error { case contactSubscribed(contact: Contact) case contactDisconnected(contact: Contact) case contactSubError(contact: Contact, chatError: ChatError) + case groupSubscribed(groupInfo: GroupInfo) + case groupEmpty(groupInfo: GroupInfo) + case userContactLinkSubscribed case newChatItem(chatItem: AChatItem) case chatCmdError(chatError: ChatError) case chatError(chatError: ChatError) @@ -111,6 +116,9 @@ enum ChatResponse: Decodable, Error { case .contactSubscribed: return "contactSubscribed" case .contactDisconnected: return "contactDisconnected" case .contactSubError: return "contactSubError" + case .groupSubscribed: return "groupSubscribed" + case .groupEmpty: return "groupEmpty" + case .userContactLinkSubscribed: return "userContactLinkSubscribed" case .newChatItem: return "newChatItem" case .chatCmdError: return "chatCmdError" case .chatError: return "chatError" @@ -143,6 +151,9 @@ enum ChatResponse: Decodable, Error { case let .contactSubscribed(contact): return String(describing: contact) case let .contactDisconnected(contact): return String(describing: contact) case let .contactSubError(contact, chatError): return "contact:\n\(String(describing: contact))\nerror:\n\(String(describing: chatError))" + case let .groupSubscribed(groupInfo): return String(describing: groupInfo) + case let .groupEmpty(groupInfo): return String(describing: groupInfo) + case .userContactLinkSubscribed: return noDetails case let .newChatItem(chatItem): return String(describing: chatItem) case let .chatCmdError(chatError): return String(describing: chatError) case let .chatError(chatError): return String(describing: chatError) @@ -187,12 +198,12 @@ enum TerminalItem: Identifiable { func chatSendCmd(_ cmd: ChatCommand) throws -> ChatResponse { var c = cmd.cmdString.cString(using: .utf8)! -// TODO some mechanism to update model without passing it - maybe Publisher / Subscriber? -// DispatchQueue.main.async { -// termId += 1 -// chatModel.terminalItems.append(.cmd(termId, cmd)) -// } - return chatResponse(chat_send_cmd(getChatCtrl(), &c)!) + let resp = chatResponse(chat_send_cmd(getChatCtrl(), &c)!) + DispatchQueue.main.async { + ChatModel.shared.terminalItems.append(.cmd(.now, cmd)) + ChatModel.shared.terminalItems.append(.resp(.now, resp)) + } + return resp } func chatRecvMsg() throws -> ChatResponse { @@ -201,7 +212,6 @@ func chatRecvMsg() throws -> ChatResponse { func apiGetActiveUser() throws -> User? { let _ = getChatCtrl() - sleep(1) let r = try chatSendCmd(.showActiveUser) switch r { case let .activeUser(user): return user @@ -305,18 +315,98 @@ func apiRejectContactRequest(contactReqId: Int64) throws { throw r } -func processReceivedMsg(_ chatModel: ChatModel, _ res: ChatResponse) { +func acceptContactRequest(_ contactRequest: UserContactRequest) { + do { + let contact = try apiAcceptContactRequest(contactReqId: contactRequest.apiId) + let chat = Chat(chatInfo: ChatInfo.direct(contact: contact), chatItems: []) + ChatModel.shared.replaceChat(contactRequest.id, chat) + } catch let error { + logger.error("acceptContactRequest error: \(error.localizedDescription)") + } +} + +func rejectContactRequest(_ contactRequest: UserContactRequest) { + do { + try apiRejectContactRequest(contactReqId: contactRequest.apiId) + ChatModel.shared.removeChat(contactRequest.id) + } catch let error { + logger.error("rejectContactRequest: \(error.localizedDescription)") + } +} + +func initializeChat() { + do { + ChatModel.shared.currentUser = try apiGetActiveUser() + } catch { + fatalError("Failed to initialize chat controller or database: \(error)") + } +} + +class ChatReceiver { + private var receiveLoop: DispatchWorkItem? + private var receiveMessages = true + private var wasStarted = false + private var _canStop = false + private var _lastMsgTime = Date.now + + static let shared = ChatReceiver() + + var lastMsgTime: Date { get { _lastMsgTime } } + + func start(bgTask: BGManager.RefreshTask? = nil) { + logger.debug("ChatReceiver.start") + wasStarted = true + receiveMessages = true + _canStop = true + _lastMsgTime = .now + if receiveLoop != nil { return } + let loop = DispatchWorkItem(qos: .default, flags: []) { + while self.receiveMessages { + do { + processReceivedMsg(try chatRecvMsg()) + self._lastMsgTime = .now + } catch { + logger.error("ChatReceiver.start chatRecvMsg error: \(error.localizedDescription)") + } + } + if let task = bgTask { task.setTaskCompleted(success: true) } + } + receiveLoop = loop + DispatchQueue.global().async(execute: loop) + } + + func stop() { + logger.debug("ChatReceiver.stop?") + if !_canStop { return } + receiveMessages = false + receiveLoop?.cancel() + receiveLoop = nil + logger.debug("ChatReceiver.stop: done") + } + + func restart() { + logger.debug("ChatReceiver.restart?") + if wasStarted && receiveLoop == nil { start() } + _canStop = false + } +} + +func processReceivedMsg(_ res: ChatResponse) { + let chatModel = ChatModel.shared DispatchQueue.main.async { chatModel.terminalItems.append(.resp(.now, res)) + logger.debug("processReceivedMsg: \(res.responseType)") switch res { case let .contactConnected(contact): chatModel.updateContact(contact) chatModel.updateNetworkStatus(contact, .connected) + NtfManager.shared.notifyContactConnected(contact) case let .receivedContactRequest(contactRequest): chatModel.addChat(Chat( chatInfo: ChatInfo.contactRequest(contactRequest: contactRequest), chatItems: [] )) + NtfManager.shared.notifyContactRequest(contactRequest) case let .contactUpdated(toContact): let cInfo = ChatInfo.direct(contact: toContact) if chatModel.hasChat(toContact.id) { @@ -338,9 +428,12 @@ func processReceivedMsg(_ chatModel: ChatModel, _ res: ChatResponse) { } chatModel.updateNetworkStatus(contact, .error(err)) case let .newChatItem(aChatItem): - chatModel.addChatItem(aChatItem.chatInfo, aChatItem.chatItem) + let cInfo = aChatItem.chatInfo + let cItem = aChatItem.chatItem + chatModel.addChatItem(cInfo, cItem) + NtfManager.shared.notifyMessageReceived(cInfo, cItem) default: - print("unsupported response: ", res.responseType) + logger.debug("unsupported event: \(res.responseType)") } } } @@ -363,7 +456,7 @@ private func chatResponse(_ cjson: UnsafePointer) -> ChatResponse { let r = try jsonDecoder.decode(APIResponse.self, from: d) return r.resp } catch { - print (error) + logger.error("chatResponse jsonDecoder.decode error: \(error.localizedDescription)") } var type: String? diff --git a/apps/ios/Shared/MyPlayground.playground/Contents.swift b/apps/ios/Shared/MyPlayground.playground/Contents.swift index 991679a8b4..eb0b6af432 100644 --- a/apps/ios/Shared/MyPlayground.playground/Contents.swift +++ b/apps/ios/Shared/MyPlayground.playground/Contents.swift @@ -28,3 +28,5 @@ for match in matches { let r = try! NSRegularExpression(pattern: "^\\+?[0-9\\.\\(\\)\\-]{7,20}$") print(r.firstMatch(in: "+44(0)7448-736-790", options: [], range: NSRange(location: 0, length: "+44(0)7448-736-790".count)) == nil) + +let action: NtfAction? = NtfAction(rawValue: "NTF_ACT_ACCEPT") diff --git a/apps/ios/Shared/MyPlayground.playground/timeline.xctimeline b/apps/ios/Shared/MyPlayground.playground/timeline.xctimeline index ad2411a0b9..f62b952eff 100644 --- a/apps/ios/Shared/MyPlayground.playground/timeline.xctimeline +++ b/apps/ios/Shared/MyPlayground.playground/timeline.xctimeline @@ -3,7 +3,7 @@ version = "3.0"> diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 35f3cad9b7..0c46429ef4 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -6,29 +6,39 @@ // import SwiftUI +import OSLog + +let logger = Logger() @main struct SimpleXApp: App { - @StateObject private var chatModel = ChatModel() - + @StateObject private var chatModel = ChatModel.shared + @Environment(\.scenePhase) var scenePhase + init() { hs_init(0, nil) + BGManager.shared.register() + NtfManager.shared.registerCategories() } var body: some Scene { - WindowGroup { + return WindowGroup { ContentView() .environmentObject(chatModel) .onOpenURL { url in + logger.debug("ContentView.onOpenURL: \(url)") chatModel.appOpenUrl = url chatModel.connectViaUrl = true - print(url) } .onAppear() { - do { - chatModel.currentUser = try apiGetActiveUser() - } catch { - fatalError("Failed to initialize chat controller or database: \(error)") + initializeChat() + } + .onChange(of: scenePhase) { phase in + if phase == .background { + BGManager.shared.schedule() + } else { + BGManager.shared.invalidateStopTimer() + ChatReceiver.shared.restart() } } } diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 6d59b19462..9799db243f 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -79,7 +79,7 @@ struct ChatInfoView: View { chatModel.removeChat(contact.id) showChatInfo = false } catch let error { - print("Error: \(error)") + logger.error("ChatInfoView.deleteContactAlert apiDeleteChat error: \(error.localizedDescription)") } alertContact = nil }, secondaryButton: .cancel() { diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 864fa39a74..ccfb4b89f1 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -98,7 +98,7 @@ struct ChatView: View { let chatItem = try apiSendMessage(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, msg: .text(msg)) chatModel.addChatItem(chat.chatInfo, chatItem) } catch { - print(error) + logger.error("ChatView.sendMessage apiSendMessage error: \(error.localizedDescription)") } } } diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index b26ed3f303..f707660477 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -40,7 +40,7 @@ struct ChatListNavLink: View { chatModel.updateChatInfo(chat.chatInfo) chatModel.chatItems = chat.chatItems } catch { - print("apiGetChatItems", error) + logger.error("ChatListNavLink.chatView apiGetChatItems error: \(error.localizedDescription)") } } } @@ -121,7 +121,7 @@ struct ChatListNavLink: View { try apiDeleteChat(type: .direct, id: contact.apiId) chatModel.removeChat(contact.id) } catch let error { - print("Error: \(error)") + logger.error("ChatListNavLink.deleteContactAlert apiDeleteChat error: \(error.localizedDescription)") } alertContact = nil }, secondaryButton: .cancel() { @@ -149,25 +149,6 @@ struct ChatListNavLink: View { } ) } - - private func acceptContactRequest(_ contactRequest: UserContactRequest) { - do { - let contact = try apiAcceptContactRequest(contactReqId: contactRequest.apiId) - let chat = Chat(chatInfo: ChatInfo.direct(contact: contact), chatItems: []) - chatModel.replaceChat(contactRequest.id, chat) - } catch let error { - print("Error: \(error)") - } - } - - private func rejectContactRequest(_ contactRequest: UserContactRequest) { - do { - try apiRejectContactRequest(contactReqId: contactRequest.apiId) - chatModel.removeChat(contactRequest.id) - } catch let error { - print("Error: \(error)") - } - } } struct ChatListNavLink_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index ba6082e9cb..bdc3c815cc 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -42,15 +42,17 @@ struct ChatListView: View { NewChatButton() } } - .alert(isPresented: $connectAlert) { connectionErrorAlert() } + .alert(isPresented: $chatModel.connectViaUrl) { connectViaUrlAlert() } } - .alert(isPresented: $chatModel.connectViaUrl) { connectViaUrlAlert() } + .alert(isPresented: $connectAlert) { connectionErrorAlert() } } } private func connectViaUrlAlert() -> Alert { + logger.debug("ChatListView.connectViaUrlAlert") if let url = chatModel.appOpenUrl { var path = url.path + logger.debug("ChatListView.connectViaUrlAlert path: \(path)") if (path == "/contact" || path == "/invitation") { path.removeFirst() let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)") @@ -63,7 +65,7 @@ struct ChatListView: View { } catch { connectAlert = true connectError = error - print(error) + logger.debug("ChatListView.connectViaUrlAlert: apiConnect error: \(error.localizedDescription)") } chatModel.appOpenUrl = nil }, secondaryButton: .cancel() { @@ -71,7 +73,7 @@ struct ChatListView: View { } ) } else { - return Alert(title: Text("Error: URL not available")) + return Alert(title: Text("Error: URL is invalid")) } } else { return Alert(title: Text("Error: URL not available")) diff --git a/apps/ios/Shared/Views/NewChat/ConnectContactView.swift b/apps/ios/Shared/Views/NewChat/ConnectContactView.swift index 088f336f9c..024c310434 100644 --- a/apps/ios/Shared/Views/NewChat/ConnectContactView.swift +++ b/apps/ios/Shared/Views/NewChat/ConnectContactView.swift @@ -37,11 +37,11 @@ struct ConnectContactView: View { try apiConnect(connReq: r.string) completed(nil) } catch { - print(error) + logger.error("ConnectContactView.processQRCode apiConnect error: \(error.localizedDescription)") completed(error) } case let .failure(e): - print(e) + logger.error("ConnectContactView.processQRCode QR code error: \(e.localizedDescription)") completed(e) } } diff --git a/apps/ios/Shared/Views/NewChat/NewChatButton.swift b/apps/ios/Shared/Views/NewChat/NewChatButton.swift index db65dd0dcb..ba495dfd21 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatButton.swift @@ -51,7 +51,7 @@ struct NewChatButton: View { } catch { addContactAlert = true addContactError = error - print(error) + logger.error("NewChatButton.addContactAction apiAddContact error: \(error.localizedDescription)") } } diff --git a/apps/ios/Shared/Views/TerminalView.swift b/apps/ios/Shared/Views/TerminalView.swift index 0170786053..2410ff37ba 100644 --- a/apps/ios/Shared/Views/TerminalView.swift +++ b/apps/ios/Shared/Views/TerminalView.swift @@ -52,17 +52,12 @@ struct TerminalView: View { func sendMessage(_ cmdStr: String) { let cmd = ChatCommand.string(cmdStr) - chatModel.terminalItems.append(.cmd(.now, cmd)) - DispatchQueue.global().async { inProgress = true do { - let r = try chatSendCmd(cmd) - DispatchQueue.main.async { - chatModel.terminalItems.append(.resp(.now, r)) - } + let _ = try chatSendCmd(cmd) } catch { - print(error) + logger.error("TerminalView.sendMessage chatSendCmd error: \(error.localizedDescription)") } inProgress = false } diff --git a/apps/ios/Shared/Views/UserSettings/SettingsButton.swift b/apps/ios/Shared/Views/UserSettings/SettingsButton.swift index 702ebbdeaf..f735214086 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsButton.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsButton.swift @@ -22,7 +22,7 @@ struct SettingsButton: View { do { chatModel.userAddress = try apiGetUserAddress() } catch { - print(error) + logger.error("SettingsButton apiGetUserAddress error: \(error.localizedDescription)") } } }) diff --git a/apps/ios/Shared/Views/UserSettings/UserAddress.swift b/apps/ios/Shared/Views/UserSettings/UserAddress.swift index 19ebf6c778..7d6c8cca65 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddress.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddress.swift @@ -39,7 +39,7 @@ struct UserAddress: View { try apiDeleteUserAddress() chatModel.userAddress = nil } catch let error { - print("Error: \(error)") + logger.error("UserAddress apiDeleteUserAddress: \(error.localizedDescription)") } }, secondaryButton: .cancel() ) @@ -52,7 +52,7 @@ struct UserAddress: View { do { chatModel.userAddress = try apiCreateUserAddress() } catch let error { - print("Error: \(error)") + logger.error("UserAddress apiCreateUserAddress: \(error.localizedDescription)") } } label: { Label("Create address", systemImage: "qrcode") } .frame(maxWidth: .infinity) diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index ac252ed838..2dd5575033 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -68,7 +68,7 @@ struct UserProfile: View { profile = newProfile } } catch { - print(error) + logger.error("UserProfile apiUpdateProfile error: \(error.localizedDescription)") } editProfile = false } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index ffd38c3569..84ada554d9 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -26,6 +26,10 @@ 5C35CF7127B031FB00FB6C6D /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C35CF6C27B031FB00FB6C6D /* libgmp.a */; }; 5C35CF7227B031FB00FB6C6D /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C35CF6D27B031FB00FB6C6D /* libffi.a */; }; 5C35CF7327B031FB00FB6C6D /* libHSsimplex-chat-1.1.0-Cr0nZom8Et1JJ5D58xT8LZ-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C35CF6E27B031FB00FB6C6D /* libHSsimplex-chat-1.1.0-Cr0nZom8Et1JJ5D58xT8LZ-ghc8.10.7.a */; }; + 5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C35CFC727B2782E00FB6C6D /* BGManager.swift */; }; + 5C35CFC927B2782E00FB6C6D /* BGManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C35CFC727B2782E00FB6C6D /* BGManager.swift */; }; + 5C35CFCB27B2E91D00FB6C6D /* NtfManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */; }; + 5C35CFCC27B2E91D00FB6C6D /* NtfManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */; }; 5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AD81227A834E300348BD7 /* NewChatButton.swift */; }; 5C6AD81427A834E300348BD7 /* NewChatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AD81227A834E300348BD7 /* NewChatButton.swift */; }; 5C764E80279C7276000C6508 /* dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E7F279C7276000C6508 /* dummy.m */; }; @@ -116,6 +120,8 @@ 5C35CF6C27B031FB00FB6C6D /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 5C35CF6D27B031FB00FB6C6D /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; 5C35CF6E27B031FB00FB6C6D /* libHSsimplex-chat-1.1.0-Cr0nZom8Et1JJ5D58xT8LZ-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.1.0-Cr0nZom8Et1JJ5D58xT8LZ-ghc8.10.7.a"; sourceTree = ""; }; + 5C35CFC727B2782E00FB6C6D /* BGManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGManager.swift; sourceTree = ""; }; + 5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NtfManager.swift; sourceTree = ""; }; 5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = ""; }; 5C6AD81227A834E300348BD7 /* NewChatButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatButton.swift; sourceTree = ""; }; 5C764E7B279C71D4000C6508 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.2.sdk/usr/lib/libiconv.tbd; sourceTree = DEVELOPER_DIR; }; @@ -251,6 +257,8 @@ 5C764E88279CBCB3000C6508 /* ChatModel.swift */, 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */, 5C9FD96A27A56D4D0075386C /* JSON.swift */, + 5C35CFC727B2782E00FB6C6D /* BGManager.swift */, + 5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */, ); path = Model; sourceTree = ""; @@ -548,6 +556,7 @@ 5C764E80279C7276000C6508 /* dummy.m in Sources */, 5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */, 5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */, + 5C35CFCB27B2E91D00FB6C6D /* NtfManager.swift in Sources */, 5C2E261227A30FEA00F70299 /* TerminalView.swift in Sources */, 5C9FD96B27A56D4D0075386C /* JSON.swift in Sources */, 5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */, @@ -556,6 +565,7 @@ 5CB9250D27A9432000ACCCDD /* ChatListNavLink.swift in Sources */, 5CA059ED279559F40002BEB4 /* ContentView.swift in Sources */, 5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */, + 5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */, 5CA05A4C27974EB60002BEB4 /* WelcomeView.swift in Sources */, 5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */, 5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */, @@ -585,6 +595,7 @@ 5C764E81279C7276000C6508 /* dummy.m in Sources */, 5CB924E527A8683A00ACCCDD /* UserAddress.swift in Sources */, 5C063D2827A4564100AEC577 /* ChatPreviewView.swift in Sources */, + 5C35CFCC27B2E91D00FB6C6D /* NtfManager.swift in Sources */, 5C2E261327A30FEA00F70299 /* TerminalView.swift in Sources */, 5C9FD96C27A56D4D0075386C /* JSON.swift in Sources */, 5C9FD96F27A5D6ED0075386C /* SendMessageView.swift in Sources */, @@ -593,6 +604,7 @@ 5CB9250E27A9432000ACCCDD /* ChatListNavLink.swift in Sources */, 5CA059EE279559F40002BEB4 /* ContentView.swift in Sources */, 5CCD403527A5F6DF00368C90 /* AddContactView.swift in Sources */, + 5C35CFC927B2782E00FB6C6D /* BGManager.swift in Sources */, 5CA05A4D27974EB60002BEB4 /* WelcomeView.swift in Sources */, 5C2E261027A30FDC00F70299 /* ChatView.swift in Sources */, 5C2E260C27A30CFA00F70299 /* ChatListView.swift in Sources */,