mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 12:19:54 +00:00
core, ios: get messages for multiple last notifications; separately get notification connections before requesting messages (to avoid acknowledgement races in case of parralel nse threads); coordinate nse threads (#5084)
* core, ios: get messages for multiple last notifications (#5047) * ios: refactor notification service (#5086) * core, ios: separately get notification connections before requesting messages; coordinate nse threads (#5085)
This commit is contained in:
parent
edf99fcd1d
commit
78510b6fd3
11 changed files with 487 additions and 233 deletions
|
@ -199,6 +199,12 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
|||
actions: [],
|
||||
intentIdentifiers: [],
|
||||
hiddenPreviewsBodyPlaceholder: NSLocalizedString("SimpleX encrypted message or connection event", comment: "notification")
|
||||
),
|
||||
UNNotificationCategory(
|
||||
identifier: ntfCategoryManyEvents,
|
||||
actions: [],
|
||||
intentIdentifiers: [],
|
||||
hiddenPreviewsBodyPlaceholder: NSLocalizedString("New events", comment: "notification")
|
||||
)
|
||||
])
|
||||
}
|
||||
|
@ -228,24 +234,24 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
|||
|
||||
func notifyContactRequest(_ user: any UserLike, _ contactRequest: UserContactRequest) {
|
||||
logger.debug("NtfManager.notifyContactRequest")
|
||||
addNotification(createContactRequestNtf(user, contactRequest))
|
||||
addNotification(createContactRequestNtf(user, contactRequest, 0))
|
||||
}
|
||||
|
||||
func notifyContactConnected(_ user: any UserLike, _ contact: Contact) {
|
||||
logger.debug("NtfManager.notifyContactConnected")
|
||||
addNotification(createContactConnectedNtf(user, contact))
|
||||
addNotification(createContactConnectedNtf(user, contact, 0))
|
||||
}
|
||||
|
||||
func notifyMessageReceived(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem) {
|
||||
logger.debug("NtfManager.notifyMessageReceived")
|
||||
if cInfo.ntfsEnabled {
|
||||
addNotification(createMessageReceivedNtf(user, cInfo, cItem))
|
||||
addNotification(createMessageReceivedNtf(user, cInfo, cItem, 0))
|
||||
}
|
||||
}
|
||||
|
||||
func notifyCallInvitation(_ invitation: RcvCallInvitation) {
|
||||
logger.debug("NtfManager.notifyCallInvitation")
|
||||
addNotification(createCallInvitationNtf(invitation))
|
||||
addNotification(createCallInvitationNtf(invitation, 0))
|
||||
}
|
||||
|
||||
func setNtfBadgeCount(_ count: Int) {
|
||||
|
|
|
@ -26,14 +26,52 @@ enum NSENotification {
|
|||
case nse(UNMutableNotificationContent)
|
||||
case callkit(RcvCallInvitation)
|
||||
case empty
|
||||
case msgInfo(NtfMsgAckInfo)
|
||||
}
|
||||
|
||||
var isCallInvitation: Bool {
|
||||
public enum NSENotificationData {
|
||||
case connectionEvent(_ user: User, _ connEntity: ConnectionEntity)
|
||||
case contactConnected(_ user: any UserLike, _ contact: Contact)
|
||||
case contactRequest(_ user: any UserLike, _ contactRequest: UserContactRequest)
|
||||
case messageReceived(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem)
|
||||
case callInvitation(_ invitation: RcvCallInvitation)
|
||||
case msgInfo(NtfMsgAckInfo)
|
||||
case noNtf
|
||||
|
||||
var callInvitation: RcvCallInvitation? {
|
||||
switch self {
|
||||
case let .nse(ntf): ntf.categoryIdentifier == ntfCategoryCallInvitation
|
||||
case .callkit: true
|
||||
case .empty: false
|
||||
case .msgInfo: false
|
||||
case let .callInvitation(invitation): invitation
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
|
||||
func notificationContent(_ badgeCount: Int) -> UNMutableNotificationContent {
|
||||
return switch self {
|
||||
case let .connectionEvent(user, connEntity): createConnectionEventNtf(user, connEntity, badgeCount)
|
||||
case let .contactConnected(user, contact): createContactConnectedNtf(user, contact, badgeCount)
|
||||
case let .contactRequest(user, contactRequest): createContactRequestNtf(user, contactRequest, badgeCount)
|
||||
case let .messageReceived(user, cInfo, cItem): createMessageReceivedNtf(user, cInfo, cItem, badgeCount)
|
||||
case let .callInvitation(invitation): createCallInvitationNtf(invitation, badgeCount)
|
||||
case .msgInfo: UNMutableNotificationContent()
|
||||
case .noNtf: UNMutableNotificationContent()
|
||||
}
|
||||
}
|
||||
|
||||
var notificationEvent: NSENotificationData? {
|
||||
return switch self {
|
||||
case .connectionEvent: self
|
||||
case .contactConnected: self
|
||||
case .contactRequest: self
|
||||
case .messageReceived: self
|
||||
case .callInvitation: self
|
||||
case .msgInfo: nil
|
||||
case .noNtf: nil
|
||||
}
|
||||
}
|
||||
|
||||
var newMsgData: (any UserLike, ChatInfo)? {
|
||||
return switch self {
|
||||
case let .messageReceived(user, cInfo, _): (user, cInfo)
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,9 +81,10 @@ enum NSENotification {
|
|||
// or when background notification is received.
|
||||
class NSEThreads {
|
||||
static let shared = NSEThreads()
|
||||
private static let queue = DispatchQueue(label: "chat.simplex.app.SimpleX-NSE.notification-threads.lock")
|
||||
static let queue = DispatchQueue(label: "chat.simplex.app.SimpleX-NSE.notification-threads.lock")
|
||||
private var allThreads: Set<UUID> = []
|
||||
private var activeThreads: [(UUID, NotificationService)] = []
|
||||
var activeThreads: [(UUID, NotificationService)] = []
|
||||
var droppedNotifications: [(ChatId, NSENotificationData)] = []
|
||||
|
||||
func newThread() -> UUID {
|
||||
NSEThreads.queue.sync {
|
||||
|
@ -64,22 +103,19 @@ class NSEThreads {
|
|||
}
|
||||
}
|
||||
|
||||
func processNotification(_ id: ChatId, _ ntf: NSENotification) async -> Void {
|
||||
var waitTime: Int64 = 5_000_000000
|
||||
while waitTime > 0 {
|
||||
if let (_, nse) = rcvEntityThread(id),
|
||||
nse.shouldProcessNtf && nse.processReceivedNtf(ntf) {
|
||||
break
|
||||
} else {
|
||||
try? await Task.sleep(nanoseconds: 10_000000)
|
||||
waitTime -= 10_000000
|
||||
}
|
||||
func processNotification(_ id: ChatId, _ ntf: NSENotificationData) async -> Void {
|
||||
if let (_, nse) = rcvEntityThread(id),
|
||||
nse.expectedMessages[id]?.shouldProcessNtf ?? false {
|
||||
nse.processReceivedNtf(id, ntf, signalReady: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func rcvEntityThread(_ id: ChatId) -> (UUID, NotificationService)? {
|
||||
NSEThreads.queue.sync {
|
||||
activeThreads.first(where: { (_, nse) in nse.receiveEntityId == id })
|
||||
// this selects the earliest thread that:
|
||||
// 1) has this connection in nse.expectedMessages
|
||||
// 2) has not completed processing messages for this connection (not ready)
|
||||
activeThreads.first(where: { (_, nse) in nse.expectedMessages[id]?.ready == false })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,31 +142,38 @@ class NSEThreads {
|
|||
}
|
||||
}
|
||||
|
||||
struct ExpectedMessage {
|
||||
var ntfConn: UserNtfConn
|
||||
var receiveConnId: String?
|
||||
var expectedMsgId: String?
|
||||
var allowedGetNextAttempts: Int
|
||||
var msgBestAttemptNtf: NSENotificationData?
|
||||
var ready: Bool
|
||||
var shouldProcessNtf: Bool
|
||||
var startedProcessingNewMsgs: Bool
|
||||
var semaphore: DispatchSemaphore
|
||||
}
|
||||
|
||||
// Notification service extension creates a new instance of the class and calls didReceive for each notification.
|
||||
// Each didReceive is called in its own thread, but multiple calls can be made in one process, and, empirically, there is never
|
||||
// more than one process of notification service extension exists at a time.
|
||||
// Soon after notification service delivers the last notification it is either suspended or terminated.
|
||||
class NotificationService: UNNotificationServiceExtension {
|
||||
var contentHandler: ((UNNotificationContent) -> Void)?
|
||||
var bestAttemptNtf: NSENotification?
|
||||
// served as notification if no message attempts (msgBestAttemptNtf) could be produced
|
||||
var serviceBestAttemptNtf: 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? = NSEThreads.shared.newThread()
|
||||
var notificationInfo: NtfMessages?
|
||||
var receiveEntityId: String?
|
||||
var receiveConnId: String?
|
||||
var expectedMessage: String?
|
||||
var allowedGetNextAttempts: Int = 3
|
||||
// return true if the message is taken - it prevents sending it to another NotificationService instance for processing
|
||||
var shouldProcessNtf = false
|
||||
var expectedMessages: Dictionary<String, ExpectedMessage> = [:] // key is receiveEntityId
|
||||
var appSubscriber: AppSubscriber?
|
||||
var returnedSuspension = false
|
||||
|
||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
logger.debug("DEBUGGING: NotificationService.didReceive")
|
||||
let ntf = if let ntf_ = request.content.mutableCopy() as? UNMutableNotificationContent { ntf_ } else { UNMutableNotificationContent() }
|
||||
setBestAttemptNtf(ntf)
|
||||
setServiceBestAttemptNtf(ntf)
|
||||
self.contentHandler = contentHandler
|
||||
registerGroupDefaults()
|
||||
let appState = appStateGroupDefault.get()
|
||||
|
@ -138,13 +181,11 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
switch appState {
|
||||
case .stopped:
|
||||
setBadgeCount()
|
||||
setBestAttemptNtf(createAppStoppedNtf())
|
||||
setServiceBestAttemptNtf(createAppStoppedNtf(badgeCount))
|
||||
deliverBestAttemptNtf()
|
||||
case .suspended:
|
||||
setBadgeCount()
|
||||
receiveNtfMessages(request, contentHandler)
|
||||
case .suspending:
|
||||
setBadgeCount()
|
||||
Task {
|
||||
let state: AppState = await withCheckedContinuation { cont in
|
||||
appSubscriber = appStateSubscriber { s in
|
||||
|
@ -171,8 +212,9 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
deliverBestAttemptNtf()
|
||||
}
|
||||
}
|
||||
default:
|
||||
deliverBestAttemptNtf()
|
||||
case .active: contentHandler(UNMutableNotificationContent())
|
||||
case .activating: contentHandler(UNMutableNotificationContent())
|
||||
case .bgRefresh: contentHandler(UNMutableNotificationContent())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -192,78 +234,165 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
if let t = threadId { NSEThreads.shared.startThread(t, self) }
|
||||
let dbStatus = startChat()
|
||||
if case .ok = dbStatus,
|
||||
let ntfInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) {
|
||||
logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo.receivedMsg_ == nil ? 0 : 1))")
|
||||
if let connEntity = ntfInfo.connEntity_ {
|
||||
setBestAttemptNtf(
|
||||
ntfInfo.ntfsEnabled
|
||||
? .nse(createConnectionEventNtf(ntfInfo.user, connEntity))
|
||||
: .empty
|
||||
)
|
||||
if let id = connEntity.id, ntfInfo.expectedMsg_ != nil {
|
||||
notificationInfo = ntfInfo
|
||||
receiveEntityId = id
|
||||
receiveConnId = connEntity.conn.agentConnId
|
||||
let expectedMsgId = ntfInfo.expectedMsg_?.msgId
|
||||
let receivedMsgId = ntfInfo.receivedMsg_?.msgId
|
||||
logger.debug("NotificationService: receiveNtfMessages: expectedMsgId = \(expectedMsgId ?? "nil", privacy: .private), receivedMsgId = \(receivedMsgId ?? "nil", privacy: .private)")
|
||||
expectedMessage = expectedMsgId
|
||||
shouldProcessNtf = true
|
||||
return
|
||||
let ntfConns = apiGetNtfConns(nonce: nonce, encNtfInfo: encNtfInfo) {
|
||||
logger.debug("NotificationService: receiveNtfMessages: apiGetNtfConns ntfConns count = \(ntfConns.count)")
|
||||
|
||||
for ntfConn in ntfConns {
|
||||
addExpectedMessage(ntfConn: ntfConn)
|
||||
}
|
||||
|
||||
let connIdsToGet = expectedMessages.compactMap { (id, _) in
|
||||
let started = NSEThreads.queue.sync {
|
||||
let canStart = checkCanStart(id)
|
||||
if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): receiveNtfMessages: can start: \(canStart)") }
|
||||
if canStart {
|
||||
processDroppedNotifications(id)
|
||||
expectedMessages[id]?.startedProcessingNewMsgs = true
|
||||
expectedMessages[id]?.shouldProcessNtf = true
|
||||
}
|
||||
return canStart
|
||||
}
|
||||
if started {
|
||||
return expectedMessages[id]?.receiveConnId
|
||||
} else {
|
||||
if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): receiveNtfMessages: entity \(id, privacy: .private) waiting on semaphore") }
|
||||
expectedMessages[id]?.semaphore.wait()
|
||||
if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): receiveNtfMessages: entity \(id, privacy: .private) proceeding after semaphore") }
|
||||
Task {
|
||||
NSEThreads.queue.sync {
|
||||
processDroppedNotifications(id)
|
||||
expectedMessages[id]?.startedProcessingNewMsgs = true
|
||||
expectedMessages[id]?.shouldProcessNtf = true
|
||||
}
|
||||
if let connId = expectedMessages[id]?.receiveConnId {
|
||||
let _ = getConnNtfMessage(connId: connId)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if !connIdsToGet.isEmpty {
|
||||
if let r = apiGetConnNtfMessages(connIds: connIdsToGet) {
|
||||
logger.debug("NotificationService: receiveNtfMessages: apiGetConnNtfMessages count = \(r.count)")
|
||||
}
|
||||
return
|
||||
}
|
||||
} else if let dbStatus = dbStatus {
|
||||
setBestAttemptNtf(createErrorNtf(dbStatus))
|
||||
setServiceBestAttemptNtf(createErrorNtf(dbStatus, badgeCount))
|
||||
}
|
||||
}
|
||||
deliverBestAttemptNtf()
|
||||
}
|
||||
|
||||
func addExpectedMessage(ntfConn: UserNtfConn) {
|
||||
if let connEntity = ntfConn.connEntity_,
|
||||
let receiveEntityId = connEntity.id, ntfConn.expectedMsg_ != nil {
|
||||
let expectedMsgId = ntfConn.expectedMsg_?.msgId
|
||||
logger.debug("NotificationService: addExpectedMessage: expectedMsgId = \(expectedMsgId ?? "nil", privacy: .private)")
|
||||
expectedMessages[receiveEntityId] = ExpectedMessage(
|
||||
ntfConn: ntfConn,
|
||||
receiveConnId: connEntity.conn.agentConnId,
|
||||
expectedMsgId: expectedMsgId,
|
||||
allowedGetNextAttempts: 3,
|
||||
msgBestAttemptNtf: ntfConn.defaultBestAttemptNtf,
|
||||
ready: false,
|
||||
shouldProcessNtf: false,
|
||||
startedProcessingNewMsgs: false,
|
||||
semaphore: DispatchSemaphore(value: 0)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func checkCanStart(_ entityId: String) -> Bool {
|
||||
return !NSEThreads.shared.activeThreads.contains(where: {
|
||||
(tId, nse) in tId != threadId && nse.expectedMessages.contains(where: { $0.key == entityId })
|
||||
})
|
||||
}
|
||||
|
||||
func processDroppedNotifications(_ entityId: String) {
|
||||
if !NSEThreads.shared.droppedNotifications.isEmpty {
|
||||
let messagesToProcess = NSEThreads.shared.droppedNotifications.filter { (eId, _) in eId == entityId }
|
||||
NSEThreads.shared.droppedNotifications.removeAll(where: { (eId, _) in eId == entityId })
|
||||
for (index, (_, ntf)) in messagesToProcess.enumerated() {
|
||||
if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): entity \(entityId, privacy: .private): processing dropped notification \(index, privacy: .private)") }
|
||||
processReceivedNtf(entityId, ntf, signalReady: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func serviceExtensionTimeWillExpire() {
|
||||
logger.debug("DEBUGGING: NotificationService.serviceExtensionTimeWillExpire")
|
||||
deliverBestAttemptNtf(urgent: true)
|
||||
}
|
||||
|
||||
func processReceivedNtf(_ ntf: NSENotification) -> Bool {
|
||||
guard let ntfInfo = notificationInfo, let expectedMsgTs = ntfInfo.expectedMsg_?.msgTs else { return false }
|
||||
if !ntfInfo.user.showNotifications {
|
||||
self.setBestAttemptNtf(.empty)
|
||||
var expectingMoreMessages: Bool {
|
||||
!expectedMessages.allSatisfy { $0.value.ready }
|
||||
}
|
||||
|
||||
func processReceivedNtf(_ id: ChatId, _ ntf: NSENotificationData, signalReady: Bool) {
|
||||
guard let expectedMessage = expectedMessages[id] else {
|
||||
return
|
||||
}
|
||||
guard let expectedMsgTs = expectedMessage.ntfConn.expectedMsg_?.msgTs else {
|
||||
NSEThreads.shared.droppedNotifications.append((id, ntf))
|
||||
if signalReady { entityReady(id) }
|
||||
return
|
||||
}
|
||||
if case let .msgInfo(info) = ntf {
|
||||
if info.msgId == expectedMessage {
|
||||
expectedMessage = nil
|
||||
if info.msgId == expectedMessage.expectedMsgId {
|
||||
logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): expected")
|
||||
expectedMessages[id]?.expectedMsgId = nil
|
||||
if signalReady { entityReady(id) }
|
||||
self.deliverBestAttemptNtf()
|
||||
return true
|
||||
} else if let msgTs = info.msgTs_, msgTs > expectedMsgTs {
|
||||
logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): unexpected msgInfo, let other instance to process it, stopping this one")
|
||||
NSEThreads.shared.droppedNotifications.append((id, ntf))
|
||||
if signalReady { entityReady(id) }
|
||||
self.deliverBestAttemptNtf()
|
||||
return false
|
||||
} else if allowedGetNextAttempts > 0, let receiveConnId = receiveConnId {
|
||||
} else if (expectedMessages[id]?.allowedGetNextAttempts ?? 0) > 0, let receiveConnId = expectedMessages[id]?.receiveConnId {
|
||||
logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): unexpected msgInfo, get next message")
|
||||
allowedGetNextAttempts -= 1
|
||||
if let receivedMsg = apiGetConnNtfMessage(connId: receiveConnId) {
|
||||
logger.debug("NotificationService processNtf, on apiGetConnNtfMessage: msgInfo msgId = \(info.msgId, privacy: .private), receivedMsg msgId = \(receivedMsg.msgId, privacy: .private)")
|
||||
return true
|
||||
expectedMessages[id]?.allowedGetNextAttempts -= 1
|
||||
if let receivedMsg = getConnNtfMessage(connId: receiveConnId) {
|
||||
logger.debug("NotificationService processNtf, on getConnNtfMessage: msgInfo msgId = \(info.msgId, privacy: .private), receivedMsg msgId = \(receivedMsg.msgId, privacy: .private)")
|
||||
} else {
|
||||
logger.debug("NotificationService processNtf, on apiGetConnNtfMessage: msgInfo msgId = \(info.msgId, privacy: .private): no next message, deliver best attempt")
|
||||
logger.debug("NotificationService processNtf, on getConnNtfMessage: msgInfo msgId = \(info.msgId, privacy: .private): no next message, deliver best attempt")
|
||||
NSEThreads.shared.droppedNotifications.append((id, ntf))
|
||||
if signalReady { entityReady(id) }
|
||||
self.deliverBestAttemptNtf()
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): unknown message, let other instance to process it")
|
||||
NSEThreads.shared.droppedNotifications.append((id, ntf))
|
||||
if signalReady { entityReady(id) }
|
||||
self.deliverBestAttemptNtf()
|
||||
return false
|
||||
}
|
||||
} else if ntfInfo.user.showNotifications {
|
||||
} else if expectedMessage.ntfConn.user.showNotifications {
|
||||
logger.debug("NotificationService processNtf: setting best attempt")
|
||||
self.setBestAttemptNtf(ntf)
|
||||
if ntf.isCallInvitation {
|
||||
self.deliverBestAttemptNtf()
|
||||
if ntf.notificationEvent != nil {
|
||||
setBadgeCount()
|
||||
}
|
||||
return true
|
||||
let prevBestAttempt = expectedMessages[id]?.msgBestAttemptNtf
|
||||
if prevBestAttempt?.callInvitation != nil {
|
||||
if ntf.callInvitation != nil { // replace with newer call
|
||||
expectedMessages[id]?.msgBestAttemptNtf = ntf
|
||||
} // otherwise keep call as best attempt
|
||||
} else {
|
||||
expectedMessages[id]?.msgBestAttemptNtf = ntf
|
||||
}
|
||||
} else {
|
||||
NSEThreads.shared.droppedNotifications.append((id, ntf))
|
||||
if signalReady { entityReady(id) }
|
||||
}
|
||||
}
|
||||
|
||||
func entityReady(_ entityId: ChatId) {
|
||||
if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): entityReady: entity \(entityId, privacy: .private)") }
|
||||
expectedMessages[entityId]?.ready = true
|
||||
if let (tNext, nse) = NSEThreads.shared.activeThreads.first(where: { (_, nse) in nse.expectedMessages[entityId]?.startedProcessingNewMsgs == false }) {
|
||||
if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): entityReady: signal next thread \(tNext, privacy: .private) for entity \(entityId, privacy: .private)") }
|
||||
nse.expectedMessages[entityId]?.semaphore.signal()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func setBadgeCount() {
|
||||
|
@ -271,37 +400,32 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
ntfBadgeCountGroupDefault.set(badgeCount)
|
||||
}
|
||||
|
||||
func setBestAttemptNtf(_ ntf: UNMutableNotificationContent) {
|
||||
setBestAttemptNtf(.nse(ntf))
|
||||
}
|
||||
|
||||
func setBestAttemptNtf(_ ntf: NSENotification) {
|
||||
logger.debug("NotificationService.setBestAttemptNtf")
|
||||
if case let .nse(notification) = ntf {
|
||||
notification.badge = badgeCount as NSNumber
|
||||
bestAttemptNtf = .nse(notification)
|
||||
} else {
|
||||
bestAttemptNtf = ntf
|
||||
}
|
||||
func setServiceBestAttemptNtf(_ ntf: UNMutableNotificationContent) {
|
||||
logger.debug("NotificationService.setServiceBestAttemptNtf")
|
||||
serviceBestAttemptNtf = .nse(ntf)
|
||||
}
|
||||
|
||||
private func deliverBestAttemptNtf(urgent: Bool = false) {
|
||||
logger.debug("NotificationService.deliverBestAttemptNtf")
|
||||
// stop processing other messages
|
||||
shouldProcessNtf = false
|
||||
if (urgent || !expectingMoreMessages) {
|
||||
logger.debug("NotificationService.deliverBestAttemptNtf")
|
||||
// stop processing other messages
|
||||
for (key, _) in expectedMessages {
|
||||
expectedMessages[key]?.shouldProcessNtf = false
|
||||
}
|
||||
|
||||
let suspend: Bool
|
||||
if let t = threadId {
|
||||
threadId = nil
|
||||
suspend = NSEThreads.shared.endThread(t) && NSEThreads.shared.noThreads
|
||||
} else {
|
||||
suspend = false
|
||||
let suspend: Bool
|
||||
if let t = threadId {
|
||||
threadId = nil
|
||||
suspend = NSEThreads.shared.endThread(t) && NSEThreads.shared.noThreads
|
||||
} else {
|
||||
suspend = false
|
||||
}
|
||||
deliverCallkitOrNotification(urgent: urgent, suspend: suspend)
|
||||
}
|
||||
deliverCallkitOrNotification(urgent: urgent, suspend: suspend)
|
||||
}
|
||||
|
||||
private func deliverCallkitOrNotification(urgent: Bool, suspend: Bool = false) {
|
||||
if case .callkit = bestAttemptNtf {
|
||||
if useCallKit() && expectedMessages.contains(where: { $0.value.msgBestAttemptNtf?.callInvitation != nil }) {
|
||||
logger.debug("NotificationService.deliverCallkitOrNotification: will suspend, callkit")
|
||||
if urgent {
|
||||
// suspending NSE even though there may be other notifications
|
||||
|
@ -339,19 +463,13 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
}
|
||||
|
||||
private func deliverNotification() {
|
||||
if let handler = contentHandler, let ntf = bestAttemptNtf {
|
||||
if let handler = contentHandler, let ntf = prepareNotification() {
|
||||
contentHandler = nil
|
||||
bestAttemptNtf = nil
|
||||
let deliver: (UNMutableNotificationContent?) -> Void = { ntf in
|
||||
let useNtf = if let ntf = ntf {
|
||||
appStateGroupDefault.get().running ? UNMutableNotificationContent() : ntf
|
||||
} else {
|
||||
UNMutableNotificationContent()
|
||||
}
|
||||
handler(useNtf)
|
||||
}
|
||||
serviceBestAttemptNtf = nil
|
||||
switch ntf {
|
||||
case let .nse(content): deliver(content)
|
||||
case let .nse(content):
|
||||
content.badge = badgeCount as NSNumber
|
||||
handler(content)
|
||||
case let .callkit(invitation):
|
||||
logger.debug("NotificationService reportNewIncomingVoIPPushPayload for \(invitation.contact.id)")
|
||||
CXProvider.reportNewIncomingVoIPPushPayload([
|
||||
|
@ -362,13 +480,85 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
"callTs": invitation.callTs.timeIntervalSince1970
|
||||
]) { error in
|
||||
logger.debug("reportNewIncomingVoIPPushPayload result: \(error)")
|
||||
deliver(error == nil ? nil : createCallInvitationNtf(invitation))
|
||||
handler(error == nil ? UNMutableNotificationContent() : createCallInvitationNtf(invitation, self.badgeCount))
|
||||
}
|
||||
case .empty: deliver(nil) // used to mute notifications that did not unsubscribe yet
|
||||
case .msgInfo: deliver(nil) // unreachable, the best attempt is never set to msgInfo
|
||||
case .empty:
|
||||
handler(UNMutableNotificationContent()) // used to mute notifications that did not unsubscribe yet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func prepareNotification() -> NSENotification? {
|
||||
if expectedMessages.isEmpty {
|
||||
return serviceBestAttemptNtf
|
||||
} else if let callNtfKV = expectedMessages.first(where: { $0.value.msgBestAttemptNtf?.callInvitation != nil }),
|
||||
let callInv = callNtfKV.value.msgBestAttemptNtf?.callInvitation,
|
||||
let callNtf = callNtfKV.value.msgBestAttemptNtf {
|
||||
return useCallKit() ? .callkit(callInv) : .nse(callNtf.notificationContent(badgeCount))
|
||||
} else {
|
||||
let ntfEvents = expectedMessages.compactMap { $0.value.msgBestAttemptNtf?.notificationEvent }
|
||||
if ntfEvents.isEmpty {
|
||||
return .empty
|
||||
} else if let ntfEvent = ntfEvents.count == 1 ? ntfEvents.first : nil {
|
||||
return .nse(ntfEvent.notificationContent(badgeCount))
|
||||
} else {
|
||||
return .nse(createJointNtf(ntfEvents))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createJointNtf(_ ntfEvents: [NSENotificationData]) -> UNMutableNotificationContent {
|
||||
let previewMode = ntfPreviewModeGroupDefault.get()
|
||||
let newMsgsData: [(any UserLike, ChatInfo)] = ntfEvents.compactMap { $0.newMsgData }
|
||||
if !newMsgsData.isEmpty, let userId = newMsgsData.first?.0.userId {
|
||||
let newMsgsChats: [ChatInfo] = newMsgsData.map { $0.1 }
|
||||
let uniqueChatsNames = uniqueNewMsgsChatsNames(newMsgsChats)
|
||||
var body: String
|
||||
if previewMode == .hidden {
|
||||
body = String.localizedStringWithFormat(NSLocalizedString("New messages in %d chats", comment: "notification body"), uniqueChatsNames.count)
|
||||
} else {
|
||||
body = String.localizedStringWithFormat(NSLocalizedString("From: %@", comment: "notification body"), newMsgsChatsNamesStr(uniqueChatsNames))
|
||||
}
|
||||
return createNotification(
|
||||
categoryIdentifier: ntfCategoryManyEvents,
|
||||
title: NSLocalizedString("New messages", comment: "notification"),
|
||||
body: body,
|
||||
userInfo: ["userId": userId],
|
||||
badgeCount: badgeCount
|
||||
)
|
||||
} else {
|
||||
return createNotification(
|
||||
categoryIdentifier: ntfCategoryManyEvents,
|
||||
title: NSLocalizedString("New events", comment: "notification"),
|
||||
body: String.localizedStringWithFormat(NSLocalizedString("%d new events", comment: "notification body"), ntfEvents.count),
|
||||
badgeCount: badgeCount
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func uniqueNewMsgsChatsNames(_ newMsgsChats: [ChatInfo]) -> [String] {
|
||||
var seenChatIds = Set<ChatId>()
|
||||
var uniqueChatsNames: [String] = []
|
||||
for chat in newMsgsChats {
|
||||
if !seenChatIds.contains(chat.id) {
|
||||
seenChatIds.insert(chat.id)
|
||||
uniqueChatsNames.append(chat.chatViewName)
|
||||
}
|
||||
}
|
||||
return uniqueChatsNames
|
||||
}
|
||||
|
||||
private func newMsgsChatsNamesStr(_ names: [String]) -> String {
|
||||
return switch names.count {
|
||||
case 1: names[0]
|
||||
case 2: "\(names[0]) and \(names[1])"
|
||||
case 3: "\(names[0] + ", " + names[1]) and \(names[2])"
|
||||
default:
|
||||
names.count > 3
|
||||
? "\(names[0]), \(names[1]) and \(names.count - 2) other chats"
|
||||
: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// nseStateGroupDefault must not be used in NSE directly, only via this singleton
|
||||
|
@ -582,28 +772,25 @@ func chatRecvMsg() async -> ChatResponse? {
|
|||
private let isInChina = SKStorefront().countryCode == "CHN"
|
||||
private func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() }
|
||||
|
||||
func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
|
||||
func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotificationData)? {
|
||||
logger.debug("NotificationService receivedMsgNtf: \(res.responseType)")
|
||||
switch res {
|
||||
case let .contactConnected(user, contact, _):
|
||||
return (contact.id, .nse(createContactConnectedNtf(user, contact)))
|
||||
return (contact.id, .contactConnected(user, contact))
|
||||
// case let .contactConnecting(contact):
|
||||
// TODO profile update
|
||||
case let .receivedContactRequest(user, contactRequest):
|
||||
return (UserContact(contactRequest: contactRequest).id, .nse(createContactRequestNtf(user, contactRequest)))
|
||||
return (UserContact(contactRequest: contactRequest).id, .contactRequest(user, contactRequest))
|
||||
case let .newChatItems(user, chatItems):
|
||||
// Received items are created one at a time
|
||||
if let chatItem = chatItems.first {
|
||||
let cInfo = chatItem.chatInfo
|
||||
var cItem = chatItem.chatItem
|
||||
if !cInfo.ntfsEnabled {
|
||||
ntfBadgeCountGroupDefault.set(max(0, ntfBadgeCountGroupDefault.get() - 1))
|
||||
}
|
||||
if let file = cItem.autoReceiveFile() {
|
||||
cItem = autoReceiveFile(file) ?? cItem
|
||||
}
|
||||
let ntf: NSENotification = cInfo.ntfsEnabled ? .nse(createMessageReceivedNtf(user, cInfo, cItem)) : .empty
|
||||
return cItem.showNotification ? (chatItem.chatId, ntf) : nil
|
||||
let ntf: NSENotificationData = (cInfo.ntfsEnabled && cItem.showNotification) ? .messageReceived(user, cInfo, cItem) : .noNtf
|
||||
return (chatItem.chatId, ntf)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
@ -620,10 +807,7 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
|
|||
return nil
|
||||
case let .callInvitation(invitation):
|
||||
// Do not post it without CallKit support, iOS will stop launching the app without showing CallKit
|
||||
return (
|
||||
invitation.contact.id,
|
||||
useCallKit() ? .callkit(invitation) : .nse(createCallInvitationNtf(invitation))
|
||||
)
|
||||
return (invitation.contact.id, .callInvitation(invitation))
|
||||
case let .ntfMessage(_, connEntity, ntfMessage):
|
||||
return if let id = connEntity.id { (id, .msgInfo(ntfMessage)) } else { nil }
|
||||
case .chatSuspended:
|
||||
|
@ -704,15 +888,15 @@ func apiSetEncryptLocalFiles(_ enable: Bool) throws {
|
|||
throw r
|
||||
}
|
||||
|
||||
func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
|
||||
func apiGetNtfConns(nonce: String, encNtfInfo: String) -> [UserNtfConn]? {
|
||||
guard apiGetActiveUser() != nil else {
|
||||
logger.debug("no active user")
|
||||
return nil
|
||||
}
|
||||
let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo))
|
||||
if case let .ntfMessages(user, connEntity_, expectedMsg_, receivedMsg_) = r, let user = user {
|
||||
logger.debug("apiGetNtfMessage response ntfMessages: \(receivedMsg_ == nil ? 0 : 1)")
|
||||
return NtfMessages(user: user, connEntity_: connEntity_, expectedMsg_: expectedMsg_, receivedMsg_: receivedMsg_)
|
||||
let r = sendSimpleXCmd(.apiGetNtfConns(nonce: nonce, encNtfInfo: encNtfInfo))
|
||||
if case let .ntfConns(ntfConns) = r {
|
||||
logger.debug("apiGetNtfConns response ntfConns: \(ntfConns.count)")
|
||||
return ntfConns.compactMap { toUserNtfConn($0) }
|
||||
} else if case let .chatCmdError(_, error) = r {
|
||||
logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))")
|
||||
} else {
|
||||
|
@ -721,17 +905,33 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
|
|||
return nil
|
||||
}
|
||||
|
||||
func apiGetConnNtfMessage(connId: String) -> NtfMsgInfo? {
|
||||
func toUserNtfConn(_ ntfConn: NtfConn) -> UserNtfConn? {
|
||||
if let user = ntfConn.user_ {
|
||||
return UserNtfConn(user: user, connEntity_: ntfConn.connEntity_, expectedMsg_: ntfConn.expectedMsg_)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func apiGetConnNtfMessages(connIds: [String]) -> [NtfMsgInfo?]? {
|
||||
guard apiGetActiveUser() != nil else {
|
||||
logger.debug("no active user")
|
||||
return nil
|
||||
}
|
||||
let r = sendSimpleXCmd(.apiGetConnNtfMessage(connId: connId))
|
||||
if case let .connNtfMessage(receivedMsg_) = r {
|
||||
logger.debug("apiGetConnNtfMessage response receivedMsg_: \(receivedMsg_ == nil ? 0 : 1)")
|
||||
return receivedMsg_
|
||||
let r = sendSimpleXCmd(.apiGetConnNtfMessages(connIds: connIds))
|
||||
if case let .connNtfMessages(receivedMsgs) = r {
|
||||
logger.debug("apiGetConnNtfMessages response receivedMsgs: \(receivedMsgs.count)")
|
||||
return receivedMsgs
|
||||
}
|
||||
logger.debug("apiGetConnNtfMessages error: \(responseError(r))")
|
||||
return nil
|
||||
}
|
||||
|
||||
func getConnNtfMessage(connId: String) -> NtfMsgInfo? {
|
||||
let r_ = apiGetConnNtfMessages(connIds: [connId])
|
||||
if let r = r_, let receivedMsg = r.count == 1 ? r.first : nil {
|
||||
return receivedMsg
|
||||
}
|
||||
logger.debug("apiGetConnNtfMessage error: \(responseError(r))")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -769,13 +969,33 @@ func setNetworkConfig(_ cfg: NetCfg) throws {
|
|||
throw r
|
||||
}
|
||||
|
||||
struct NtfMessages {
|
||||
struct UserNtfConn {
|
||||
var user: User
|
||||
var connEntity_: ConnectionEntity?
|
||||
var expectedMsg_: NtfMsgInfo?
|
||||
var receivedMsg_: NtfMsgInfo?
|
||||
|
||||
var ntfsEnabled: Bool {
|
||||
user.showNotifications && (connEntity_?.ntfsEnabled ?? false)
|
||||
var defaultBestAttemptNtf: NSENotificationData {
|
||||
return if !user.showNotifications {
|
||||
.noNtf
|
||||
} else if let connEntity = connEntity_ {
|
||||
switch connEntity {
|
||||
case let .rcvDirectMsgConnection(_, contact):
|
||||
contact?.chatSettings.enableNtfs == .all
|
||||
? .connectionEvent(user, connEntity)
|
||||
: .noNtf
|
||||
case let .rcvGroupMsgConnection(_, groupInfo, _):
|
||||
groupInfo.chatSettings.enableNtfs == .all
|
||||
? .connectionEvent(user, connEntity)
|
||||
: .noNtf
|
||||
case .sndFileConnection: .noNtf
|
||||
case .rcvFileConnection: .noNtf
|
||||
case let .userContactConnection(_, userContact):
|
||||
userContact.groupId == nil
|
||||
? .connectionEvent(user, connEntity)
|
||||
: .noNtf
|
||||
}
|
||||
} else {
|
||||
.noNtf
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -148,6 +148,11 @@
|
|||
6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */; };
|
||||
6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; };
|
||||
6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; };
|
||||
643B3B452CCBEB080083A2CF /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B402CCBEB080083A2CF /* libgmpxx.a */; };
|
||||
643B3B462CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a */; };
|
||||
643B3B472CCBEB080083A2CF /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B422CCBEB080083A2CF /* libffi.a */; };
|
||||
643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a */; };
|
||||
643B3B492CCBEB080083A2CF /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B442CCBEB080083A2CF /* libgmp.a */; };
|
||||
6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; };
|
||||
6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; };
|
||||
6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */; };
|
||||
|
@ -220,15 +225,10 @@
|
|||
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; };
|
||||
D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; };
|
||||
E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51CC1E52C62085600DB91FE /* OneHandUICard.swift */; };
|
||||
E56F46202CC2B2E300F1559D /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E56F461E2CC2B2E300F1559D /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a */; };
|
||||
E56F46212CC2B2E300F1559D /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E56F461F2CC2B2E300F1559D /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a */; };
|
||||
E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; };
|
||||
E5DCF9712C590272007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF96F2C590272007928CC /* Localizable.strings */; };
|
||||
E5DCF9842C5902CE007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9822C5902CE007928CC /* Localizable.strings */; };
|
||||
E5DCF9982C5906FF007928CC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9962C5906FF007928CC /* InfoPlist.strings */; };
|
||||
E5E997C92CBA891A00D7A2FA /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5E997C42CBA891A00D7A2FA /* libgmpxx.a */; };
|
||||
E5E997CA2CBA891A00D7A2FA /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5E997C52CBA891A00D7A2FA /* libgmp.a */; };
|
||||
E5E997CC2CBA891A00D7A2FA /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5E997C72CBA891A00D7A2FA /* libffi.a */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
@ -491,6 +491,11 @@
|
|||
6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = "<group>"; };
|
||||
6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = "<group>"; };
|
||||
6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = "<group>"; };
|
||||
643B3B402CCBEB080083A2CF /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmpxx.a; path = Libraries/libgmpxx.a; sourceTree = "<group>"; };
|
||||
643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a"; path = "Libraries/libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
643B3B422CCBEB080083A2CF /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libffi.a; path = Libraries/libffi.a; sourceTree = "<group>"; };
|
||||
643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a"; path = "Libraries/libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a"; sourceTree = "<group>"; };
|
||||
643B3B442CCBEB080083A2CF /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmp.a; path = Libraries/libgmp.a; sourceTree = "<group>"; };
|
||||
6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = "<group>"; };
|
||||
6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = "<group>"; };
|
||||
6442E0B9287F169300CEC0F9 /* AddGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupView.swift; sourceTree = "<group>"; };
|
||||
|
@ -562,8 +567,6 @@
|
|||
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||
D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||
E51CC1E52C62085600DB91FE /* OneHandUICard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneHandUICard.swift; sourceTree = "<group>"; };
|
||||
E56F461E2CC2B2E300F1559D /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a"; sourceTree = "<group>"; };
|
||||
E56F461F2CC2B2E300F1559D /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
E5DCF9702C590272007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
E5DCF9722C590274007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
E5DCF9732C590275007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
|
@ -616,9 +619,6 @@
|
|||
E5DCF9A62C590731007928CC /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
E5DCF9A72C590732007928CC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
E5DCF9A82C590732007928CC /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
E5E997C42CBA891A00D7A2FA /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
E5E997C52CBA891A00D7A2FA /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
E5E997C72CBA891A00D7A2FA /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -657,14 +657,14 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E5E997C92CBA891A00D7A2FA /* libgmpxx.a in Frameworks */,
|
||||
E5E997CA2CBA891A00D7A2FA /* libgmp.a in Frameworks */,
|
||||
643B3B452CCBEB080083A2CF /* libgmpxx.a in Frameworks */,
|
||||
643B3B472CCBEB080083A2CF /* libffi.a in Frameworks */,
|
||||
643B3B492CCBEB080083A2CF /* libgmp.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
E5E997CC2CBA891A00D7A2FA /* libffi.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
E56F46202CC2B2E300F1559D /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a in Frameworks */,
|
||||
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
|
||||
E56F46212CC2B2E300F1559D /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a in Frameworks */,
|
||||
643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a in Frameworks */,
|
||||
643B3B462CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -741,11 +741,6 @@
|
|||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E5E997C72CBA891A00D7A2FA /* libffi.a */,
|
||||
E5E997C52CBA891A00D7A2FA /* libgmp.a */,
|
||||
E5E997C42CBA891A00D7A2FA /* libgmpxx.a */,
|
||||
E56F461F2CC2B2E300F1559D /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a */,
|
||||
E56F461E2CC2B2E300F1559D /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
|
@ -817,6 +812,11 @@
|
|||
5CC2C0FA2809BF11000C35E3 /* Localizable.strings */,
|
||||
5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */,
|
||||
5C764E5C279C70B7000C6508 /* Libraries */,
|
||||
643B3B422CCBEB080083A2CF /* libffi.a */,
|
||||
643B3B442CCBEB080083A2CF /* libgmp.a */,
|
||||
643B3B402CCBEB080083A2CF /* libgmpxx.a */,
|
||||
643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a */,
|
||||
643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a */,
|
||||
5CA059C2279559F40002BEB4 /* Shared */,
|
||||
5CDCAD462818589900503DA2 /* SimpleX NSE */,
|
||||
CEE723A82C3BD3D70009AE93 /* SimpleX SE */,
|
||||
|
|
|
@ -55,8 +55,8 @@ public enum ChatCommand {
|
|||
case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode)
|
||||
case apiVerifyToken(token: DeviceToken, nonce: String, code: String)
|
||||
case apiDeleteToken(token: DeviceToken)
|
||||
case apiGetNtfMessage(nonce: String, encNtfInfo: String)
|
||||
case apiGetConnNtfMessage(connId: String)
|
||||
case apiGetNtfConns(nonce: String, encNtfInfo: String)
|
||||
case apiGetConnNtfMessages(connIds: [String])
|
||||
case apiNewGroup(userId: Int64, incognito: Bool, groupProfile: GroupProfile)
|
||||
case apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole)
|
||||
case apiJoinGroup(groupId: Int64)
|
||||
|
@ -214,8 +214,8 @@ public enum ChatCommand {
|
|||
case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)"
|
||||
case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)"
|
||||
case let .apiDeleteToken(token): return "/_ntf delete \(token.cmdString)"
|
||||
case let .apiGetNtfMessage(nonce, encNtfInfo): return "/_ntf message \(nonce) \(encNtfInfo)"
|
||||
case let .apiGetConnNtfMessage(connId): return "/_ntf conn message \(connId)"
|
||||
case let .apiGetNtfConns(nonce, encNtfInfo): return "/_ntf conns \(nonce) \(encNtfInfo)"
|
||||
case let .apiGetConnNtfMessages(connIds): return "/_ntf conn messages \(connIds.joined(separator: ","))"
|
||||
case let .apiNewGroup(userId, incognito, groupProfile): return "/_group \(userId) incognito=\(onOff(incognito)) \(encodeJSON(groupProfile))"
|
||||
case let .apiAddMember(groupId, contactId, memberRole): return "/_add #\(groupId) \(contactId) \(memberRole)"
|
||||
case let .apiJoinGroup(groupId): return "/_join #\(groupId)"
|
||||
|
@ -369,8 +369,8 @@ public enum ChatCommand {
|
|||
case .apiRegisterToken: return "apiRegisterToken"
|
||||
case .apiVerifyToken: return "apiVerifyToken"
|
||||
case .apiDeleteToken: return "apiDeleteToken"
|
||||
case .apiGetNtfMessage: return "apiGetNtfMessage"
|
||||
case .apiGetConnNtfMessage: return "apiGetConnNtfMessage"
|
||||
case .apiGetNtfConns: return "apiGetNtfConns"
|
||||
case .apiGetConnNtfMessages: return "apiGetConnNtfMessages"
|
||||
case .apiNewGroup: return "apiNewGroup"
|
||||
case .apiAddMember: return "apiAddMember"
|
||||
case .apiJoinGroup: return "apiJoinGroup"
|
||||
|
@ -682,8 +682,8 @@ public enum ChatResponse: Decodable, Error {
|
|||
case callInvitations(callInvitations: [RcvCallInvitation])
|
||||
case ntfTokenStatus(status: NtfTknStatus)
|
||||
case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode, ntfServer: String)
|
||||
case ntfMessages(user_: User?, connEntity_: ConnectionEntity?, expectedMsg_: NtfMsgInfo?, receivedMsg_: NtfMsgInfo?)
|
||||
case connNtfMessage(receivedMsg_: NtfMsgInfo?)
|
||||
case ntfConns(ntfConns: [NtfConn])
|
||||
case connNtfMessages(receivedMsgs: [NtfMsgInfo?])
|
||||
case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgAckInfo)
|
||||
case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection)
|
||||
case contactDisabled(user: UserRef, contact: Contact)
|
||||
|
@ -851,8 +851,8 @@ public enum ChatResponse: Decodable, Error {
|
|||
case .callInvitations: return "callInvitations"
|
||||
case .ntfTokenStatus: return "ntfTokenStatus"
|
||||
case .ntfToken: return "ntfToken"
|
||||
case .ntfMessages: return "ntfMessages"
|
||||
case .connNtfMessage: return "connNtfMessage"
|
||||
case .ntfConns: return "ntfConns"
|
||||
case .connNtfMessages: return "connNtfMessages"
|
||||
case .ntfMessage: return "ntfMessage"
|
||||
case .contactConnectionDeleted: return "contactConnectionDeleted"
|
||||
case .contactDisabled: return "contactDisabled"
|
||||
|
@ -1029,8 +1029,8 @@ public enum ChatResponse: Decodable, Error {
|
|||
case let .callInvitations(invs): return String(describing: invs)
|
||||
case let .ntfTokenStatus(status): return String(describing: status)
|
||||
case let .ntfToken(token, status, ntfMode, ntfServer): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)\nntfServer: \(ntfServer)"
|
||||
case let .ntfMessages(u, connEntity, expectedMsg_, receivedMsg_): return withUser(u, "connEntity: \(String(describing: connEntity))\nexpectedMsg_: \(String(describing: expectedMsg_))\nreceivedMsg_: \(String(describing: receivedMsg_))")
|
||||
case let .connNtfMessage(receivedMsg_): return "receivedMsg_: \(String(describing: receivedMsg_))"
|
||||
case let .ntfConns(ntfConns): return String(describing: ntfConns)
|
||||
case let .connNtfMessages(receivedMsgs): return "receivedMsgs: \(String(describing: receivedMsgs))"
|
||||
case let .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))")
|
||||
case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection))
|
||||
case let .contactDisabled(u, contact): return withUser(u, String(describing: contact))
|
||||
|
|
|
@ -2270,16 +2270,13 @@ public enum ConnectionEntity: Decodable, Hashable {
|
|||
case let .userContactConnection(entityConnection, _): entityConnection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct NtfConn: Decodable, Hashable {
|
||||
public var user_: User?
|
||||
public var connEntity_: ConnectionEntity?
|
||||
public var expectedMsg_: NtfMsgInfo?
|
||||
|
||||
public var ntfsEnabled: Bool {
|
||||
switch self {
|
||||
case let .rcvDirectMsgConnection(_, contact): return contact?.chatSettings.enableNtfs == .all
|
||||
case let .rcvGroupMsgConnection(_, groupInfo, _): return groupInfo.chatSettings.enableNtfs == .all
|
||||
case .sndFileConnection: return false
|
||||
case .rcvFileConnection: return false
|
||||
case let .userContactConnection(_, userContact): return userContact.groupId == nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct NtfMsgInfo: Decodable, Hashable {
|
||||
|
|
|
@ -15,13 +15,14 @@ public let ntfCategoryContactConnected = "NTF_CAT_CONTACT_CONNECTED"
|
|||
public let ntfCategoryMessageReceived = "NTF_CAT_MESSAGE_RECEIVED"
|
||||
public let ntfCategoryCallInvitation = "NTF_CAT_CALL_INVITATION"
|
||||
public let ntfCategoryConnectionEvent = "NTF_CAT_CONNECTION_EVENT"
|
||||
public let ntfCategoryManyEvents = "NTF_CAT_MANY_EVENTS"
|
||||
public let ntfCategoryCheckMessage = "NTF_CAT_CHECK_MESSAGE"
|
||||
|
||||
public let appNotificationId = "chat.simplex.app.notification"
|
||||
|
||||
let contactHidden = NSLocalizedString("Contact hidden:", comment: "notification")
|
||||
|
||||
public func createContactRequestNtf(_ user: any UserLike, _ contactRequest: UserContactRequest) -> UNMutableNotificationContent {
|
||||
public func createContactRequestNtf(_ user: any UserLike, _ contactRequest: UserContactRequest, _ badgeCount: Int) -> UNMutableNotificationContent {
|
||||
let hideContent = ntfPreviewModeGroupDefault.get() == .hidden
|
||||
return createNotification(
|
||||
categoryIdentifier: ntfCategoryContactRequest,
|
||||
|
@ -34,11 +35,12 @@ public func createContactRequestNtf(_ user: any UserLike, _ contactRequest: User
|
|||
hideContent ? NSLocalizedString("this contact", comment: "notification title") : contactRequest.chatViewName
|
||||
),
|
||||
targetContentIdentifier: nil,
|
||||
userInfo: ["chatId": contactRequest.id, "contactRequestId": contactRequest.apiId, "userId": user.userId]
|
||||
userInfo: ["chatId": contactRequest.id, "contactRequestId": contactRequest.apiId, "userId": user.userId],
|
||||
badgeCount: badgeCount
|
||||
)
|
||||
}
|
||||
|
||||
public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact) -> UNMutableNotificationContent {
|
||||
public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact, _ badgeCount: Int) -> UNMutableNotificationContent {
|
||||
let hideContent = ntfPreviewModeGroupDefault.get() == .hidden
|
||||
return createNotification(
|
||||
categoryIdentifier: ntfCategoryContactConnected,
|
||||
|
@ -51,12 +53,13 @@ public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact)
|
|||
hideContent ? NSLocalizedString("this contact", comment: "notification title") : contact.chatViewName
|
||||
),
|
||||
targetContentIdentifier: contact.id,
|
||||
userInfo: ["userId": user.userId]
|
||||
userInfo: ["userId": user.userId],
|
||||
// userInfo: ["chatId": contact.id, "contactId": contact.apiId]
|
||||
badgeCount: badgeCount
|
||||
)
|
||||
}
|
||||
|
||||
public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutableNotificationContent {
|
||||
public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem, _ badgeCount: Int) -> UNMutableNotificationContent {
|
||||
let previewMode = ntfPreviewModeGroupDefault.get()
|
||||
var title: String
|
||||
if case let .group(groupInfo) = cInfo, case let .groupRcv(groupMember) = cItem.chatDir {
|
||||
|
@ -69,12 +72,13 @@ public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _
|
|||
title: title,
|
||||
body: previewMode == .message ? hideSecrets(cItem) : NSLocalizedString("new message", comment: "notification"),
|
||||
targetContentIdentifier: cInfo.id,
|
||||
userInfo: ["userId": user.userId]
|
||||
userInfo: ["userId": user.userId],
|
||||
// userInfo: ["chatId": cInfo.id, "chatItemId": cItem.id]
|
||||
badgeCount: badgeCount
|
||||
)
|
||||
}
|
||||
|
||||
public func createCallInvitationNtf(_ invitation: RcvCallInvitation) -> UNMutableNotificationContent {
|
||||
public func createCallInvitationNtf(_ invitation: RcvCallInvitation, _ badgeCount: Int) -> UNMutableNotificationContent {
|
||||
let text = invitation.callType.media == .video
|
||||
? NSLocalizedString("Incoming video call", comment: "notification")
|
||||
: NSLocalizedString("Incoming audio call", comment: "notification")
|
||||
|
@ -84,11 +88,12 @@ public func createCallInvitationNtf(_ invitation: RcvCallInvitation) -> UNMutabl
|
|||
title: hideContent ? contactHidden : "\(invitation.contact.chatViewName):",
|
||||
body: text,
|
||||
targetContentIdentifier: nil,
|
||||
userInfo: ["chatId": invitation.contact.id, "userId": invitation.user.userId]
|
||||
userInfo: ["chatId": invitation.contact.id, "userId": invitation.user.userId],
|
||||
badgeCount: badgeCount
|
||||
)
|
||||
}
|
||||
|
||||
public func createConnectionEventNtf(_ user: User, _ connEntity: ConnectionEntity) -> UNMutableNotificationContent {
|
||||
public func createConnectionEventNtf(_ user: User, _ connEntity: ConnectionEntity, _ badgeCount: Int) -> UNMutableNotificationContent {
|
||||
let hideContent = ntfPreviewModeGroupDefault.get() == .hidden
|
||||
var title: String
|
||||
var body: String? = nil
|
||||
|
@ -118,11 +123,12 @@ public func createConnectionEventNtf(_ user: User, _ connEntity: ConnectionEntit
|
|||
title: title,
|
||||
body: body,
|
||||
targetContentIdentifier: targetContentIdentifier,
|
||||
userInfo: ["userId": user.userId]
|
||||
userInfo: ["userId": user.userId],
|
||||
badgeCount: badgeCount
|
||||
)
|
||||
}
|
||||
|
||||
public func createErrorNtf(_ dbStatus: DBMigrationResult) -> UNMutableNotificationContent {
|
||||
public func createErrorNtf(_ dbStatus: DBMigrationResult, _ badgeCount: Int) -> UNMutableNotificationContent {
|
||||
var title: String
|
||||
switch dbStatus {
|
||||
case .errorNotADatabase:
|
||||
|
@ -142,14 +148,16 @@ public func createErrorNtf(_ dbStatus: DBMigrationResult) -> UNMutableNotificati
|
|||
}
|
||||
return createNotification(
|
||||
categoryIdentifier: ntfCategoryConnectionEvent,
|
||||
title: title
|
||||
title: title,
|
||||
badgeCount: badgeCount
|
||||
)
|
||||
}
|
||||
|
||||
public func createAppStoppedNtf() -> UNMutableNotificationContent {
|
||||
public func createAppStoppedNtf(_ badgeCount: Int) -> UNMutableNotificationContent {
|
||||
return createNotification(
|
||||
categoryIdentifier: ntfCategoryConnectionEvent,
|
||||
title: NSLocalizedString("Encrypted message: app is stopped", comment: "notification")
|
||||
title: NSLocalizedString("Encrypted message: app is stopped", comment: "notification"),
|
||||
badgeCount: badgeCount
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -159,8 +167,15 @@ private func groupMsgNtfTitle(_ groupInfo: GroupInfo, _ groupMember: GroupMember
|
|||
: "#\(groupInfo.displayName) \(groupMember.chatViewName):"
|
||||
}
|
||||
|
||||
public func createNotification(categoryIdentifier: String, title: String, subtitle: String? = nil, body: String? = nil,
|
||||
targetContentIdentifier: String? = nil, userInfo: [AnyHashable : Any] = [:]) -> UNMutableNotificationContent {
|
||||
public func createNotification(
|
||||
categoryIdentifier: String,
|
||||
title: String,
|
||||
subtitle: String? = nil,
|
||||
body: String? = nil,
|
||||
targetContentIdentifier: String? = nil,
|
||||
userInfo: [AnyHashable : Any] = [:],
|
||||
badgeCount: Int
|
||||
) -> UNMutableNotificationContent {
|
||||
let content = UNMutableNotificationContent()
|
||||
content.categoryIdentifier = categoryIdentifier
|
||||
content.title = title
|
||||
|
@ -170,6 +185,7 @@ public func createNotification(categoryIdentifier: String, title: String, subtit
|
|||
content.userInfo = userInfo
|
||||
// TODO move logic of adding sound here, so it applies to background notifications too
|
||||
content.sound = .default
|
||||
content.badge = badgeCount as NSNumber
|
||||
// content.interruptionLevel = .active
|
||||
// content.relevanceScore = 0.5 // 0-1
|
||||
return content
|
||||
|
|
|
@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
|||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: 967afaf802d7ea98480eaf280bfc6f35d4d43f05
|
||||
tag: a8471eed5be93e7c3741aa4742b24193c9a2d6f5
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."967afaf802d7ea98480eaf280bfc6f35d4d43f05" = "0k8m07hxfgn8h8pqrfchqd8490fvv1jf8slw8qjp0vxdpxa84n3i";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."a8471eed5be93e7c3741aa4742b24193c9a2d6f5" = "093i40api0dp7rvw6f1f3pww3q5iv6mvbj577nlxp3qqcbvyh6fs";
|
||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
||||
|
|
|
@ -204,8 +204,8 @@ _defaultSMPServers =
|
|||
_defaultNtfServers :: [NtfServer]
|
||||
_defaultNtfServers =
|
||||
[ "ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im,5ex3mupcazy3zlky64ab27phjhijpemsiby33qzq3pliejipbtx5xgad.onion"
|
||||
-- "ntf://KmpZNNXiVZJx_G2T7jRUmDFxWXM3OAnunz3uLT0tqAA=@ntf3.simplex.im,pxculznuryunjdvtvh6s6szmanyadumpbmvevgdpe4wk5c65unyt4yid.onion",
|
||||
-- "ntf://CJ5o7X6fCxj2FFYRU2KuCo70y4jSqz7td2HYhLnXWbU=@ntf4.simplex.im,wtvuhdj26jwprmomnyfu5wfuq2hjkzfcc72u44vi6gdhrwxldt6xauad.onion"
|
||||
-- "ntf://KmpZNNXiVZJx_G2T7jRUmDFxWXM3OAnunz3uLT0tqAA=@ntf3.simplex.im,pxculznuryunjdvtvh6s6szmanyadumpbmvevgdpe4wk5c65unyt4yid.onion",
|
||||
-- "ntf://CJ5o7X6fCxj2FFYRU2KuCo70y4jSqz7td2HYhLnXWbU=@ntf4.simplex.im,wtvuhdj26jwprmomnyfu5wfuq2hjkzfcc72u44vi6gdhrwxldt6xauad.onion"
|
||||
]
|
||||
|
||||
maxImageSize :: Integer
|
||||
|
@ -1457,26 +1457,31 @@ processChatCommand' vr = \case
|
|||
CRNtfTokenStatus <$> withAgent (\a -> registerNtfToken a token mode)
|
||||
APIVerifyToken token nonce code -> withUser $ \_ -> withAgent (\a -> verifyNtfToken a token nonce code) >> ok_
|
||||
APIDeleteToken token -> withUser $ \_ -> withAgent (`deleteNtfToken` token) >> ok_
|
||||
APIGetNtfMessage nonce encNtfInfo -> withUser $ \_ -> do
|
||||
(NotificationInfo {ntfConnId, ntfMsgMeta = nMsgMeta}, msg) <- withAgent $ \a -> getNotificationMessage a nonce encNtfInfo
|
||||
let agentConnId = AgentConnId ntfConnId
|
||||
user_ <- withStore' (`getUserByAConnId` agentConnId)
|
||||
connEntity_ <-
|
||||
pure user_ $>>= \user ->
|
||||
withStore (\db -> Just <$> getConnectionEntity db vr user agentConnId) `catchChatError` (\e -> toView (CRChatError (Just user) e) $> Nothing)
|
||||
pure
|
||||
CRNtfMessages
|
||||
{ user_,
|
||||
connEntity_,
|
||||
-- Decrypted ntf meta of the expected message (the one notification was sent for)
|
||||
expectedMsg_ = expectedMsgInfo <$> nMsgMeta,
|
||||
-- Info of the first message retrieved by agent using GET
|
||||
-- (may differ from the expected message due to, for example, coalescing or loss of notifications)
|
||||
receivedMsg_ = receivedMsgInfo <$> msg
|
||||
}
|
||||
ApiGetConnNtfMessage (AgentConnId connId) -> withUser $ \_ -> do
|
||||
msg <- withAgent $ \a -> getConnectionMessage a connId
|
||||
pure $ CRConnNtfMessage (receivedMsgInfo <$> msg)
|
||||
APIGetNtfConns nonce encNtfInfo -> withUser $ \user -> do
|
||||
ntfInfos <- withAgent $ \a -> getNotificationConns a nonce encNtfInfo
|
||||
(errs, ntfMsgs) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (getMsgConn db) (L.toList ntfInfos))
|
||||
unless (null errs) $ toView $ CRChatErrors (Just user) errs
|
||||
pure $ CRNtfConns ntfMsgs
|
||||
where
|
||||
getMsgConn :: DB.Connection -> NotificationInfo -> IO NtfConn
|
||||
getMsgConn db NotificationInfo {ntfConnId, ntfMsgMeta = nMsgMeta} = do
|
||||
let agentConnId = AgentConnId ntfConnId
|
||||
user_ <- getUserByAConnId db agentConnId
|
||||
connEntity_ <-
|
||||
pure user_ $>>= \user ->
|
||||
eitherToMaybe <$> runExceptT (getConnectionEntity db vr user agentConnId)
|
||||
pure $
|
||||
NtfConn
|
||||
{ user_,
|
||||
connEntity_,
|
||||
-- Decrypted ntf meta of the expected message (the one notification was sent for)
|
||||
expectedMsg_ = expectedMsgInfo <$> nMsgMeta
|
||||
}
|
||||
ApiGetConnNtfMessages connIds -> withUser $ \_ -> do
|
||||
let acIds = L.map (\(AgentConnId acId) -> acId) connIds
|
||||
msgs <- lift $ withAgent' $ \a -> getConnectionMessages a acIds
|
||||
let ntfMsgs = L.map (\msg -> receivedMsgInfo <$> msg) msgs
|
||||
pure $ CRConnNtfMessages ntfMsgs
|
||||
APIGetUserProtoServers userId (AProtocolType p) -> withUserId userId $ \user -> withServerProtocol p $ do
|
||||
cfg@ChatConfig {defaultServers} <- asks config
|
||||
servers <- withFastStore' (`getProtocolServers` user)
|
||||
|
@ -1785,7 +1790,8 @@ processChatCommand' vr = \case
|
|||
Nothing -> joinNewConn chatV dm
|
||||
Just (RcvDirectMsgConnection conn@Connection {connId, connStatus, contactConnInitiated} Nothing)
|
||||
| connStatus == ConnNew && contactConnInitiated -> joinNewConn chatV dm -- own connection link
|
||||
| connStatus == ConnPrepared -> do -- retrying join after error
|
||||
| connStatus == ConnPrepared -> do
|
||||
-- retrying join after error
|
||||
pcc <- withFastStore $ \db -> getPendingContactConnection db userId connId
|
||||
joinPreparedConn (aConnId conn) pcc dm
|
||||
Just ent -> throwChatError $ CECommandError $ "connection exists: " <> show (connEntityInfo ent)
|
||||
|
@ -8061,8 +8067,8 @@ chatCommandP =
|
|||
"/_ntf register " *> (APIRegisterToken <$> strP_ <*> strP),
|
||||
"/_ntf verify " *> (APIVerifyToken <$> strP <* A.space <*> strP <* A.space <*> strP),
|
||||
"/_ntf delete " *> (APIDeleteToken <$> strP),
|
||||
"/_ntf message " *> (APIGetNtfMessage <$> strP <* A.space <*> strP),
|
||||
"/_ntf conn message " *> (ApiGetConnNtfMessage <$> strP),
|
||||
"/_ntf conns " *> (APIGetNtfConns <$> strP <* A.space <*> strP),
|
||||
"/_ntf conn messages " *> (ApiGetConnNtfMessages <$> strP),
|
||||
"/_add #" *> (APIAddMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole),
|
||||
"/_join #" *> (APIJoinGroup <$> A.decimal),
|
||||
"/_member role #" *> (APIMemberRole <$> A.decimal <* A.space <*> A.decimal <*> memberRole),
|
||||
|
|
|
@ -330,8 +330,8 @@ data ChatCommand
|
|||
| APIRegisterToken DeviceToken NotificationsMode
|
||||
| APIVerifyToken DeviceToken C.CbNonce ByteString
|
||||
| APIDeleteToken DeviceToken
|
||||
| APIGetNtfMessage {nonce :: C.CbNonce, encNtfInfo :: ByteString}
|
||||
| ApiGetConnNtfMessage {connId :: AgentConnId}
|
||||
| APIGetNtfConns {nonce :: C.CbNonce, encNtfInfo :: ByteString}
|
||||
| ApiGetConnNtfMessages {connIds :: NonEmpty AgentConnId}
|
||||
| APIAddMember GroupId ContactId GroupMemberRole
|
||||
| APIJoinGroup GroupId
|
||||
| APIMemberRole GroupId GroupMemberId GroupMemberRole
|
||||
|
@ -745,8 +745,8 @@ data ChatResponse
|
|||
| CRUserContactLinkSubError {chatError :: ChatError} -- TODO delete
|
||||
| CRNtfTokenStatus {status :: NtfTknStatus}
|
||||
| CRNtfToken {token :: DeviceToken, status :: NtfTknStatus, ntfMode :: NotificationsMode, ntfServer :: NtfServer}
|
||||
| CRNtfMessages {user_ :: Maybe User, connEntity_ :: Maybe ConnectionEntity, expectedMsg_ :: Maybe NtfMsgInfo, receivedMsg_ :: Maybe NtfMsgInfo}
|
||||
| CRConnNtfMessage {receivedMsg_ :: Maybe NtfMsgInfo}
|
||||
| CRNtfConns {ntfConns :: [NtfConn]}
|
||||
| CRConnNtfMessages {receivedMsgs :: NonEmpty (Maybe NtfMsgInfo)}
|
||||
| CRNtfMessage {user :: User, connEntity :: ConnectionEntity, ntfMessage :: NtfMsgAckInfo}
|
||||
| CRContactConnectionDeleted {user :: User, connection :: PendingContactConnection}
|
||||
| CRRemoteHostList {remoteHosts :: [RemoteHostInfo]}
|
||||
|
@ -1010,7 +1010,7 @@ defaultSimpleNetCfg =
|
|||
smpWebPort = False,
|
||||
tcpTimeout_ = Nothing,
|
||||
logTLSErrors = False
|
||||
}
|
||||
}
|
||||
|
||||
data ContactSubStatus = ContactSubStatus
|
||||
{ contact :: Contact,
|
||||
|
@ -1063,6 +1063,13 @@ instance FromJSON ComposedMessage where
|
|||
parseJSON invalid =
|
||||
JT.prependFailure "bad ComposedMessage, " (JT.typeMismatch "Object" invalid)
|
||||
|
||||
data NtfConn = NtfConn
|
||||
{ user_ :: Maybe User,
|
||||
connEntity_ :: Maybe ConnectionEntity,
|
||||
expectedMsg_ :: Maybe NtfMsgInfo
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data NtfMsgInfo = NtfMsgInfo {msgId :: Text, msgTs :: UTCTime}
|
||||
deriving (Show)
|
||||
|
||||
|
@ -1535,6 +1542,8 @@ $(JQ.deriveJSON defaultJSON ''UserProfileUpdateSummary)
|
|||
|
||||
$(JQ.deriveJSON defaultJSON ''NtfMsgInfo)
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''NtfConn)
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''NtfMsgAckInfo)
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''SwitchProgress)
|
||||
|
|
|
@ -325,8 +325,8 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
|
|||
CRContactConnectionDeleted u PendingContactConnection {pccConnId} -> ttyUser u ["connection :" <> sShow pccConnId <> " deleted"]
|
||||
CRNtfTokenStatus status -> ["device token status: " <> plain (smpEncode status)]
|
||||
CRNtfToken _ status mode srv -> ["device token status: " <> plain (smpEncode status) <> ", notifications mode: " <> plain (strEncode mode) <> ", server: " <> sShow srv]
|
||||
CRNtfMessages {} -> []
|
||||
CRConnNtfMessage {} -> []
|
||||
CRNtfConns {} -> []
|
||||
CRConnNtfMessages {} -> []
|
||||
CRNtfMessage {} -> []
|
||||
CRCurrentRemoteHost rhi_ ->
|
||||
[ maybe
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue