receiving messages in the background and sending local notifications (#284)

* receiving messages in the background and sending local notifications

* show notifications in foreground and background

* presentation logic for notification options when app is in the foreground

* background refresh works

* remove async dispatch
This commit is contained in:
Evgeny Poberezkin 2022-02-09 22:53:06 +00:00 committed by GitHub
parent ff7a8cade1
commit 516c8d79ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 508 additions and 98 deletions

View File

@ -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()
}
}
}
}

View File

@ -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
}
}
}

View File

@ -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

View File

@ -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<ChatId, Date> = [:]
// 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)
}
}

View File

@ -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<CChar>) -> 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?

View File

@ -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")

View File

@ -3,7 +3,7 @@
version = "3.0">
<TimelineItems>
<LoggerValueHistoryTimelineItem
documentLocation = "file:///Users/evgeny/opensource/simplex-chat/simplex-chat/apps/ios/Shared/MyPlayground.playground#CharacterRangeLen=88&amp;CharacterRangeLoc=91&amp;EndingColumnNumber=0&amp;EndingLineNumber=7&amp;StartingColumnNumber=3&amp;StartingLineNumber=6&amp;Timestamp=665873333.107271"
documentLocation = "file:///Users/evgeny/opensource/simplex-chat/simplex-chat/apps/ios/Shared/MyPlayground.playground#CharacterRangeLen=88&amp;CharacterRangeLoc=91&amp;EndingColumnNumber=0&amp;EndingLineNumber=7&amp;StartingColumnNumber=3&amp;StartingLineNumber=6&amp;Timestamp=666087303.155273"
selectedRepresentationIndex = "0"
shouldTrackSuperviewWidth = "NO">
</LoggerValueHistoryTimelineItem>

View File

@ -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()
}
}
}

View File

@ -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() {

View File

@ -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)")
}
}
}

View File

@ -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 {

View File

@ -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"))

View File

@ -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)
}
}

View File

@ -51,7 +51,7 @@ struct NewChatButton: View {
} catch {
addContactAlert = true
addContactError = error
print(error)
logger.error("NewChatButton.addContactAction apiAddContact error: \(error.localizedDescription)")
}
}

View File

@ -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
}

View File

@ -22,7 +22,7 @@ struct SettingsButton: View {
do {
chatModel.userAddress = try apiGetUserAddress()
} catch {
print(error)
logger.error("SettingsButton apiGetUserAddress error: \(error.localizedDescription)")
}
}
})

View File

@ -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)

View File

@ -68,7 +68,7 @@ struct UserProfile: View {
profile = newProfile
}
} catch {
print(error)
logger.error("UserProfile apiUpdateProfile error: \(error.localizedDescription)")
}
editProfile = false
}

View File

@ -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 = "<group>"; };
5C35CF6D27B031FB00FB6C6D /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
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 = "<group>"; };
5C35CFC727B2782E00FB6C6D /* BGManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGManager.swift; sourceTree = "<group>"; };
5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NtfManager.swift; sourceTree = "<group>"; };
5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = "<group>"; };
5C6AD81227A834E300348BD7 /* NewChatButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatButton.swift; sourceTree = "<group>"; };
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 = "<group>";
@ -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 */,