@ -22,12 +22,6 @@ let nseSuspendSchedule: SuspendSchedule = (2, 4)
let fastNSESuspendSchedule : SuspendSchedule = ( 1 , 1 )
enum NSENotification {
case nse ( UNMutableNotificationContent )
case callkit ( RcvCallInvitation )
case empty
}
public enum NSENotificationData {
case connectionEvent ( _ user : User , _ connEntity : ConnectionEntity )
case contactConnected ( _ user : any UserLike , _ contact : Contact )
@ -37,6 +31,7 @@ public enum NSENotificationData {
case msgInfo ( NtfMsgAckInfo )
case noNtf
@ inline ( __always )
var callInvitation : RcvCallInvitation ? {
switch self {
case let . callInvitation ( invitation ) : invitation
@ -56,8 +51,9 @@ public enum NSENotificationData {
}
}
@ inline ( __always )
var notificationEvent : NSENotificationData ? {
return switch self {
switch self {
case . connectionEvent : self
case . contactConnected : self
case . contactRequest : self
@ -68,9 +64,10 @@ public enum NSENotificationData {
}
}
var newMsgData : ( any UserLike , ChatInfo ) ? {
return switch self {
case let . messageReceived ( user , cInfo , _ ) : ( user , cInfo )
@ inline ( __always )
var newMsgNtf : NSENotificationData ? {
switch self {
case . messageReceived : self
default : nil
}
}
@ -81,20 +78,25 @@ public enum NSENotificationData {
// o r w h e n b a c k g r o u n d n o t i f i c a t i o n i s r e c e i v e d .
class NSEThreads {
static let shared = NSEThreads ( )
static let queue = DispatchQueue ( label : " chat.simplex.app.SimpleX-NSE.notification-threads.lock " )
private let queue = DispatchQueue ( label : " chat.simplex.app.SimpleX-NSE.notification-threads.lock " )
private var allThreads : Set < UUID > = [ ]
var activeThreads : [ ( UUID, NotificationService ) ] = [ ]
var droppedNotifications : [ ( ChatId, NSENotificationData ) ] = [ ]
private var activeThreads : [ ( threadId: UUID , nse : NotificationService ) ] = [ ]
private var droppedNotifications : [ ( entityId: ChatId , ntf : NSENotificationData ) ] = [ ]
@ inline ( __always )
private init ( ) { } // o n l y s h a r e d i n s t a n c e c a n b e u s e d
@ inline ( __always )
func newThread ( ) -> UUID {
NSEThreads . queue . sync {
queue. sync {
let ( _ , t ) = allThreads . insert ( UUID ( ) )
return t
}
}
@ inline ( __always )
func startThread ( _ t : UUID , _ service : NotificationService ) {
NSEThreads. queue. sync {
queue. sync {
if allThreads . contains ( t ) {
activeThreads . append ( ( t , service ) )
} else {
@ -103,24 +105,111 @@ class NSEThreads {
}
}
// a t o m i c a l l y :
// - c h e c k s t h a t p a s s e d N S E i n s t a n c e c a n s t a r t p r o c e s s i n g p a s s e d n o t i f i c a t i o n e n t i t y ,
// - a d d s i t t o t h e p a s s e d N S E i n s t a n c e ,
// - m a r k s a s s t a r t e d , i f n o o t h e r N S E i n s t a n c e i s p r o c e s s i n g i t .
// M a k i n g a l l t h e s e s t e p s a t o m i c p r e v e n t s a r a c e c o n d i t i o n b e t w e e n t h r e a d s w h e n b o t h w i l l b e a d d e d a n d n o n e w i l l b e s t a r t e d
@ inline ( __always )
func startEntity ( _ nse : NotificationService , _ ntfEntity : NotificationEntity ) -> Bool {
queue . sync {
// c h e c k i n g t h a t n o n e o f a c t i v e T h r e a d s w i t h a n o t h e r N S E i n s t a n c e p r o c e s s e s t h e s a m e e n t i t y a n d i s n o t r e a d y
let canStart = ! activeThreads . contains ( where : { ( tId , otherNSE ) in
tId != nse . threadId
&& otherNSE . notificationEntities . contains ( where : { ( id , otherEntity ) in
id = = ntfEntity . entityId
&& otherEntity . expectedMsg != nil
} )
} )
// a t o m i c a l l y a d d e n t i t y t o p a s s e d N S E i n s t a n c e
let id = ntfEntity . entityId
nse . notificationEntities [ id ] = ntfEntity
if canStart {
// a n d s e t a s s t a r t e d , s o i t c a n n o t b e c h o s e n t o s t a r t b y a n o t h e r N S E e n t i t y i n n e x t T h r e a d
nse . notificationEntities [ id ] ? . startedProcessingNewMsgs = true
}
return canStart
}
}
@ inline ( __always )
func addDroppedNtf ( _ id : ChatId , _ ntf : NSENotificationData ) {
queue . sync { droppedNotifications . append ( ( id , ntf ) ) }
}
// a t o m i c a l l y r e m o v e a n d r e t u r n f i r s t d r o p p e d n o t i f i c a t i o n f o r t h e p a s s e d e n t i t y
@ inline ( __always )
func takeDroppedNtf ( _ ntfEntity : NotificationEntity ) -> ( entityId : ChatId , ntf : NSENotificationData ) ? {
queue . sync {
if droppedNotifications . isEmpty {
nil
} else if let i = droppedNotifications . firstIndex ( where : { ( id , _ ) in id = = ntfEntity . entityId } ) {
droppedNotifications . remove ( at : i )
} else {
nil
}
}
}
// p a s s e s n o t i f i c a t i o n f o r p r o c e s s i n g t o N S E i n s t a n c e c h o s e n b y r c v E n t i t y T h r e a d
@ inline ( __always )
func processNotification ( _ id : ChatId , _ ntf : NSENotificationData ) async -> Void {
if let ( _ , nse ) = rcvEntityThread ( id ) ,
nse . expectedMessages [ id ] ? . shouldProcessNtf ? ? false {
nse . processReceivedNtf ( id , ntf , signalReady : true )
if let ( nse , ntfEntity , expectedMsg ) = rcvEntityThread ( id , ntf ) {
logger . debug ( " NotificationService processNotification \( id ) : found nse thread expecting message " )
if nse . processReceivedNtf ( ntfEntity , expectedMsg , ntf ) {
nse . finalizeEntity ( id )
}
}
}
private func rcvEntityThread ( _ id : ChatId ) -> ( UUID , NotificationService ) ? {
NSEThreads . queue . sync {
// a t o m i c a l l y :
// - c h o o s e s a c t i v e N S E i n s t a n c e t h a t i s r e a d y t o p r o c e s s n o t i f i c a t i o n s a n d e x p e c t s m e s s a g e f o r p a s s e d e n t i t y I D
// - r e t u r n s a l l d e p e n d e n c i e s f o r p r o c e s s i n g ( n o t i f i c a t i o n e n t i t y a n d e x p e c t e d m e s s a g e )
// - a d d s n o t i f i c a t i o n t o d r o p p e d N o t i f i c a t i o n s i f n o r e a d y N S E i n s t a n c e i s f o u n d f o r t h e e n t i t y
@ inline ( __always )
private func rcvEntityThread ( _ id : ChatId , _ ntf : NSENotificationData ) -> ( NotificationService , NotificationEntity , NtfMsgInfo ) ? {
queue . sync {
// t h i s s e l e c t s t h e e a r l i e s t t h r e a d t h a t :
// 1 ) h a s t h i s c o n n e c t i o n i n n s e . e x p e c t e d M e s s a g e s
// 2 ) h a s n o t c o m p l e t e d p r o c e s s i n g m e s s a g e s f o r t h i s c o n n e c t i o n ( n o t r e a d y )
activeThreads . first ( where : { ( _ , nse ) in nse . expectedMessages [ id ] ? . ready = = false } )
// 1 ) h a s t h i s c o n n e c t i o n e n t i t y i n n s e . n o t i f i c a t i o n E n t i t i t e s
// 2 ) h a s n o t c o m p l e t e d p r o c e s s i n g m e s s a g e s f o r t h i s c o n n e c t i o n e n t i t y ( n o t r e a d y )
let r = activeThreads . lazy . compactMap ( { ( _ , nse ) in
let ntfEntity = nse . notificationEntities [ id ]
return if let ntfEntity , let expectedMsg = ntfEntity . expectedMsg , ntfEntity . shouldProcessNtf {
( nse , ntfEntity , expectedMsg )
} else {
nil
}
} ) . first
if r = = nil { droppedNotifications . append ( ( id , ntf ) ) }
return r
}
}
// A t o m i c a l l y m a r k e n t i t y i n t h e p a s s e d N S E i n s t a n c e a s n o t e x p e c t i n g m e s s a g e s ,
// a n d s i g n a l t h e n e x t N S E i n s t a n c e w i t h t h i s e n t i t y t o s t a r t i t s p r o c e s s i n g .
@ inline ( __always )
func signalNextThread ( _ nse : NotificationService , _ id : ChatId ) {
queue . sync {
nse . notificationEntities [ id ] ? . expectedMsg = nil
nse . notificationEntities [ id ] ? . shouldProcessNtf = false
let next = activeThreads . first ( where : { ( _ , nseNext ) in
if let ntfEntity = nseNext . notificationEntities [ id ] {
ntfEntity . expectedMsg != nil && ! ntfEntity . startedProcessingNewMsgs
} else {
false
}
} )
if let ( tNext , nseNext ) = next {
if let t = nse . threadId { logger . debug ( " NotificationService thread \( t ) : signalNextThread: signal next thread \( tNext ) for entity \( id ) " ) }
nseNext . notificationEntities [ id ] ? . startedProcessingNewMsgs = true
nseNext . notificationEntities [ id ] ? . semaphore . signal ( )
}
}
}
@ inline ( __always )
func endThread ( _ t : UUID ) -> Bool {
NSEThreads . queue . sync {
queue. sync {
let tActive : UUID ? = if let index = activeThreads . firstIndex ( where : { $0 . 0 = = t } ) {
activeThreads . remove ( at : index ) . 0
} else {
@ -137,24 +226,49 @@ class NSEThreads {
}
}
@ inline ( __always )
var noThreads : Bool {
allThreads . isEmpty
}
}
struct ExpectedMessage {
// N o t i f i c a t i o n E n t i t y i s a p r o c e s s i n g s t a t e f o r n o t i f i c a t i o n s f r o m a s i n g l e c o n n e c t i o n e n t i t y ( m e s s a g e q u e u e ) .
// E a c h N S E i n s t a n c e w i t h i n N S E p r o c e s s c a n h a v e m o r e t h a n o n e N o t i f i c a t i o n E n t i t y .
// N o t i f i c a t i o n E n t i t i e s o f a n N S E i n s t a n c e a r e p r o c e s s e d c o n c u r r e n t l y , a s m e s s a g e s a r r i v e i n a n y o r d e r .
// N o t i f i c a t i o n E n t i t i e s f o r t h e s a m e c o n n e c t i o n a c r o s s m u l t i p l e N S E i n s t a n c e s ( N S E T h r e a d s ) a r e p r o c e s s e d s e q u e n t i a l l y , s o t h a t t h e e a r l i e s t N S E i n s t a n c e r e c e i v e s t h e e a r l i e s t m e s s a g e s .
// T h e r e a s o n f o r t h i s c o m p l e x i t y i s t o p r o c e s s a l l r e q u i r e d m e s s a g e s w i t h i n a l l o t t e d 3 0 s e c o n d s ,
// a c c o u n t i n g f o r t h e p o s s i b i l i t y t h a t m u l t i p l e n o t i f i c a t i o n s m a y b e d e l i v e r e d c o n c u r r e n t l y .
struct NotificationEntity {
var ntfConn : NtfConn
var expectedMsgId : String ?
var allowedGetNextAttempts : Int
var msgBestAttemptNtf : NSENotificationData ?
var ready : Bool
var shouldProcessNtf : Bool
var startedProcessingNewMsgs : Bool
var semaphore : DispatchSemaphore
var entityId : ChatId
// e x p e c t e d M s g = = n i l m e a n s t h a t e n t i t y a l r e a d y h a s t h e b e s t a t t e m p t t o d e l i v e r , a n d n o m o r e m e s s a g e s a r e e x p e c t e d .
// I t h a p p e n s w h e n :
// - t h e u s e r i s m u t e d ( s e t t o n i l i n m k N o t i f i c a t i o n E n t i t y )
// - a p i G e t N t f C o n n s r e t u r n s t h a t t h e r e a r e n o n e w m e s s a g e s ( m s g I d i n n o t i f i c a t i o n m a t c h e s p r e v i o u s l y r e c e i v e d ) ,
// - m e s s a g i n g s e r v e r f a i l s t o r e s p o n d o r r e p l i e s t h a t t h e r e a r e n o m e s s a g e s ( a p i G e t C o n n N t f M e s s a g e s / g e t C o n n N t f M e s s a g e ) ,
// - t h e m e s s a g e i s r e c e i v e d w i t h t h e c o r r e c t I D o r t i m e s t a m p ( s e t t o n i l i n s i g n a l N e x t T h r e a d ) .
var expectedMsg : NtfMsgInfo ?
var allowedGetNextAttempts : Int = 3
var msgBestAttemptNtf : NSENotificationData
// s t a r t e d P r o c e s s i n g N e w M s g s d e t e r m i n e s t h a t t h e e n t i t y s t a r e d p r o c e s s i n g e v e n t s o n c e i t p r o c e s s e d d r o p p e d n o t i f i c a t i o n s .
// I t r e m a i n s t r u e w h e n s h o u l d P r o c e s s N t f i s s e t t o f a l s e , t o p r e v e n t N S E f r o m b e i n g c h o s e n a s t h e n e x t f o r t h e e n t i t y .
// I t i s a t o m i c a l l y s e t t o t r u e b y s t a r t T h e a d o r b y n e x t T h r e a d
var startedProcessingNewMsgs : Bool = false
// s h o u l d P r o c e s s N t f d e t e r m i n e s t h a t N S E s h o u l d p r o c e s s e v e n t s f o r t h i s e n t i t y ,
// i t i s a t o m i c a l l y s e t :
// - t o t r u e i n p r o c e s s D r o p p e d N o t i f i c a t i o n s i n c a s e d r o p p e d n o t i f i c a t i o n i s n o t c h o s e n f o r d e l i v e r y , a n d m o r e m e s s a g e s a r e n e e d e d .
// - t o f a l s e i n n e x t T h r e a d
var shouldProcessNtf : Bool = false
// t h i s s e m a p h o n e i s u s e d t o w a i t f o r a n o t h e r N S E i n s t a n c e p r o c e s s i n g e v e n t s f o r t h e s a m e e n t i t y
var semaphore : DispatchSemaphore = DispatchSemaphore ( value : 0 )
var connMsgReq : ConnMsgReq ? {
if let expectedMsg_ = ntfConn . expectedMsg_ {
ConnMsgReq ( msgConnId : ntfConn . agentConnId , msgDbQueueId : ntfConn . agentDbQueueId , msgTs : expectedMsg_ . msgTs )
if let expectedMsg {
ConnMsgReq ( msgConnId : ntfConn . agentConnId , msgDbQueueId : ntfConn . agentDbQueueId , msgTs : expectedMsg . msgTs )
} else {
nil
}
@ -168,12 +282,12 @@ struct ExpectedMessage {
class NotificationService : UNNotificationServiceExtension {
var contentHandler : ( ( UNNotificationContent ) -> Void ) ?
// s e r v e d a s n o t i f i c a t i o n i f n o m e s s a g e a t t e m p t s ( m s g B e s t A t t e m p t N t f ) c o u l d b e p r o d u c e d
var serviceBestAttemptNtf : NSENotification ?
var serviceBestAttemptNtf : UNMutableNotificationContent ?
var badgeCount : Int = 0
// t h r e a d i s a d d e d t o a l l T h r e a d s h e r e - i f t h r e a d d i d n o t s t a r t c h a t ,
// c h a t d o e s n o t n e e d t o b e s u s p e n d e d b u t N S E s t a t e s t i l l n e e d s t o b e s e t t o " s u s p e n d e d " .
var threadId : UUID ? = NSEThreads . shared . newThread ( )
var expectedMessages: Dictionary < String , ExpectedMessage > = [ : ] // k e y i s re c e i v e E n t i t y I d
var notificationEntities: Dictionary < String , NotificationEntity > = [ : ] // k e y i s e n t i t y I d
var appSubscriber : AppSubscriber ?
var returnedSuspension = false
@ -199,12 +313,15 @@ class NotificationService: UNNotificationServiceExtension {
setExpirationTimer ( )
receiveNtfMessages ( request )
case . suspending :
// w h i l e a p p l i c a t i o n i s s u s p e n d i n g , t h e c u r r e n t i n s t a n c e w i l l b e w a i t i n g
setExpirationTimer ( )
Task {
let state : AppState = await withCheckedContinuation { cont in
// t h i s s u b s c r i b e r u s e s m e s s a g e d e l i v e r y v i a N S F i l e C o o r d i n a t o r t o c o m m u n i c a t e b e t w e e n t h e a p p a n d N S E
appSubscriber = appStateSubscriber { s in
if s = = . suspended { appSuspension ( s ) }
}
// t h i s i s a f a l l b a c k t i m e o u t , i n c a s e m e s s a g e f r o m t h e a p p d o e s n o t a r r i v e
DispatchQueue . global ( ) . asyncAfter ( deadline : . now ( ) + Double ( appSuspendTimeout ) + 1 ) {
logger . debug ( " NotificationService: appSuspension timeout " )
appSuspension ( appStateGroupDefault . get ( ) )
@ -232,12 +349,18 @@ class NotificationService: UNNotificationServiceExtension {
}
}
// T h i s t i m e r c o m p e n s a t e s f o r t h e s c e n a r i o s w h e n s e r v i c e E x t e n s i o n T i m e W i l l E x p i r e d o e s n o t f i r e a t a l l .
// I t i s n o t c l e a r w h y i n s o m e c a s e s i t d o e s n o t f i r e , p o s s i b l y i t i s a b u g ,
// o r i t d e p e n d s o n w h a t t h e c u r r e n t t h r e a d i s d o i n g a t t h e m o m e n t .
// I f n o t i f i c a t i o n i s n o t d e l i v e r e d a n d n o t c a n c e l l e d , n o f u r t h e r n o t i f i c a t i o n s w i l l b e p r o c e s s e d .
@ inline ( __always )
private func setExpirationTimer ( ) -> Void {
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 30 ) {
self . deliverBestAttemptNtf ( urgent : true )
}
}
@ inline ( __always )
private func ntfRequestData ( _ request : UNNotificationRequest ) -> ( nonce : String , encNtfInfo : String ) ? {
if let ntfData = request . content . userInfo [ " notificationData " ] as ? [ AnyHashable : Any ] ,
let nonce = ntfData [ " nonce " ] as ? String ,
@ -248,6 +371,29 @@ class NotificationService: UNNotificationServiceExtension {
}
}
// T h i s f u n c t i o n t r i g g e r s n o t i f i c a t i o n m e s s a g e d e l i v e r y f o r c o n n e c t i o n e n t i t i e s r e f e r e n c e d i n t h e n o t i f i c a t i o n .
// N o t i f i c a t i o n m a y r e f e r e n c e m u l t i p l e c o n n e c t i o n e n t i t i e s ( m e s s a g e q u e u e s ) i n o r d e r t o c o m p e n s a t e f o r A p p l e s e r v e r s
// o n l y d e l i v e r i n g t h e l a t e s t n o t i f i c a t i o n , s o i t a l l o w s r e c e i v i n g m e s s a g e s f r o m u p t o 6 c o n t a c t s a n d g r o u p s f r o m a
// s i n g l e n o t i f i c a t i o n . T h i s a g g r e g a t i o n i s h a n d l e d b y a n o t i f i c a t i o n s e r v e r a n d i s d e l i v e r e d v i a A P N S s e r v e r s i n
// e 2 e e n c r y p t e d e n v e l o p e , a n d t h e a p p c o r e p r e v e n t s d u p l i c a t e p r o c e s s i n g b y k e e p i n g t r a c k o f t h e l a s t p r o c e s s e d m e s s a g e .
// T h e p r o c e s s s t e p s :
// 0 . a p i G e t C o n n N t f M e s s a g e s o r g e t C o n n N t f M e s s a g e g e t m e s s a g e s f r o m t h e s e r v e r f o r p a s s e d c o n n e c t i o n e n t i t i e s .
// W e d o n ' t k n o w i n a d v a n c e w h i c h c h a t e v e n t s w i l l b e d e l i v e r e d f r o m a p p c o r e f o r a g i v e n n o t i f i c a t i o n ,
// i t m a y b e a m e s s a g e , b u t i t c a n a l s o b e c o n t a c t r e q u e s t , v a r i o u s p r o t o c o l c o n f i r m a t i o n s , c a l l s , e t c . ,
// t h i s f u n c t i o n o n l y r e t u r n s m e t a d a t a f o r t h e e x p e c t e d c h a t e v e n t s .
// T h i s m e t a d a t a i s c o r r e l a t e d w i t h . n t f M e s s a g e c o r e e v e n t / . m s g I n f o n o t i f i c a t i o n m a r k e r -
// t h i s m a r k e r a l l o w s d e t e r m i n i n g w h e n s o m e m e s s a g e c o m p l e t e d p r o c e s s i n g .
// 1 . r e c e i v e M e s s a g e s : s i n g l e t o n l o o p r e c e i v i n g e v e n t s f r o m c o r e .
// 2 . r e c e i v e d M s g N t f : m a p s c o r e e v e n t s t o n o t i f i c a t i o n e v e n t s .
// 3 . N S E T h r e a d s . s h a r e d . p r o c e s s N o t i f i c a t i o n : c h o o s e s w h i c h n o t i f i c a t i o n s e r v i c e i n s t a n c e i n t h e c u r r e n t p r o c e s s s h o u l d p r o c e s s n o t i f i c a t i o n .
// W h i l e m o s t o f t h e t i m e w e o b s e r v e t h a t n o t i f i c a t i o n s a r e d e l i v e r e d s e q u e n t i a l l y , n o t h i n g i n t h e d o c u m e n t a t i o n c o n f i r m s i t i s s e q u e n t i a l ,
// a n d f r o m v a r i o u s s o u r c e s i t f o l l o w s t h a t e a c h i n s t a n c e e x e c u t e s i n i t s o w n t h r e a d , s o c o n c u r r e n c y i s e x p e c t e d .
// 4 . p r o c e s s R e c e i v e d N t f : o n e o f t h e i n s t a n c e s o f N S E p r o c e s s e s n o t i f i c a t i o n e v e n t , d e c i d i n g w h e t h e r t o r e q u e s t f u r t h e r m e s s a g e s
// f o r a g i v e n c o n n e c t i o n e n t i t y ( v i a g e t C o n n N t f M e s s a g e ) o r t h a t t h e c o r r e c t m e s s a g e w a s r e c e i v e d a n d n o t i f i c a t i o n c a n b e d e l i v e r e d ( d e l i v e r B e s t A t t e m p t N t f ) .
// I t i s b a s e d o n . m s g I n f o m a r k e r s t h a t i n d i c a t e t h a t m e s s a g e w i t h a g i v e n t i m e s t a m p w a s p r o c e s s e d .
// 5 . d e l i v e r B e s t A t t e m p t N t f : i s c a l l e d m u l t i p l e t i m e s , o n c e e a c h c o n n e c t i o n r e c e i v e s e n o u g h m e s s a g e s ( b a s e d o n . m s g I n f o m a r k e r ) .
// I f f u r t h e r m e s s a g e s a r e e x p e c t e d , t h i s f u n c t i o n d o e s n o t h i n g ( u n l e s s i t i s c a l l e d w i t h u r g e n t f l a g f r o m t i m e o u t / e x p i r a t i o n h a n d l e r s ) .
func receiveNtfMessages ( _ request : UNNotificationRequest ) {
logger . debug ( " NotificationService: receiveNtfMessages " )
if case . documents = dbContainerGroupDefault . get ( ) {
@ -255,95 +401,115 @@ class NotificationService: UNNotificationServiceExtension {
return
}
if let nrData = ntfRequestData ( request ) ,
// ch e c k i t h e r e a g a i n
// Ch e c k t h a t t h e a p p i s s t i l l i n a c t i v e b e f o r e s t a r t i n g t h e c o r e .
appStateGroupDefault . get ( ) . inactive {
// t h r e a d i s a d d e d t o a c t i v e T h r e a d s t r a c k i n g s e t h e r e - i f t h r e a d s t a r t e d c h a t i t n e e d s t o b e s u s p e n d e d
if let t = threadId { NSEThreads . shared . startThread ( t , self ) }
guard let t = threadId else { return }
NSEThreads . shared . startThread ( t , self )
let dbStatus = startChat ( )
// I f d a t a b a s e i s o p e n e d s u c c e s s f u l l y , g e t t h e l i s t o f c o n n e c t i o n e n t i t i e s ( g r o u p m e m b e r s , c o n t a c t s )
// t h a t a r e r e f e r e n c e d i n t h e e n c r y p t e d n o t i f i c a t i o n m e t a d a t a .
if case . ok = dbStatus ,
let ntfConns = apiGetNtfConns ( nonce : nrData . nonce , encNtfInfo : nrData . encNtfInfo ) {
logger . debug ( " NotificationService: receiveNtfMessages: apiGetNtfConns ntfConns count = \( ntfConns . count ) " )
// l o g g e r . d e b u g ( " N o t i f i c a t i o n S e r v i c e : r e c e i v e N t f M e s s a g e s : a p i G e t N t f C o n n s n t f C o n n s \ ( S t r i n g ( d e s c r i b i n g : n t f C o n n s . m a p { $ 0 . c o n n E n t i t y . i d } ) ) " )
for ntfConn in ntfConns {
addExpectedMessage ( ntfConn : ntfConn )
}
// u n c o m m e n t l o c a l D i s p l a y N a m e i n C o n n e c t i o n E n t i t y
// l o g g e r . d e b u g ( " N o t i f i c a t i o n S e r v i c e : r e c e i v e N t f M e s s a g e s : a p i G e t N t f C o n n s n t f C o n n s \ ( S t r i n g ( d e s c r i b i n g : n t f C o n n s . m a p { $ 0 . c o n n E n t i t y . l o c a l D i s p l a y N a m e } ) ) " )
let connMsgReqs = 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 ] ? . connMsgReq
// P r e p a r e e x p e c t e d m e s s a g e s - t h e y w i l l b e d e l i v e r e d t o t h e r e c e p t i o n l o o p i n t h i s c h a i n :
// T h e y a r e a t o m i c a l l y a d d e d t o t h e i n s t a n c e n o t i f i c a t i o n E n t i t i e s i n s i d e m s g R e q s l o o p , t o a v o i d a n y r a c e c o n d i t i o n s .
let ntfEntities = ntfConns . compactMap ( mkNotificationEntity )
// c o l l e c t n o t i f i c a t i o n m e s s a g e r e q u e s t s f o r a l l c o n n e c t i o n e n t i t i e s
let msgReqs : [ ( chatId : String , connMsgReq : ConnMsgReq ) ] = ntfEntities . compactMap { ntfEntity -> ( chatId : String , connMsgReq : ConnMsgReq ) ? in
// N o n e e d t o r e q u e s t m e s s a g e s f o r c o n n e c t i o n e n t i t i e s t h a t a r e " r e a d y " ,
// e . g . f o r m u t e d u s e r s o r w h e n t h e m e s s a g e i s n o t e x p e c t e d b a s e d o n n o t i f i c a t i o n .
let id = ntfEntity . entityId
if let expectedMsg = ntfEntity . expectedMsg {
if NSEThreads . shared . startEntity ( self , ntfEntity ) { // a t o m i c a l l y c h e c k s a n d a d d s n t f E n t i t y t o N S E
// p r o c e s s a n y n o t i f i c a t i o n s " p o s t p o n e d " b y t h e p r e v i o u s i n s t a n c e
let completed = processDroppedNotifications ( ntfEntity , expectedMsg )
return if ! completed , let connMsgReq = notificationEntities [ id ] ? . connMsgReq {
( id , connMsgReq )
} 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
nil
}
if let connMsgReq = expectedMessages [ id ] ? . connMsgReq {
let _ = getConnNtfMessage ( connMsgReq : connMsgReq )
} else {
// w a i t f o r a n o t h e r i n s t a n c e p r o c e s s i n g t h e s a m e c o n n e c t i o n e n t i t y
logger . debug ( " NotificationService thread \( t , privacy : . private ) : receiveNtfMessages: entity \( id , privacy : . private ) waiting on semaphore " )
// t h i s s e m a p h o r e w i l l b e r e l e a s e d b y s i g n a l N e x t T h r e a d f u n c t i o n , t h a t l o o k s u p t h e i n s t a n c e
// w a i t i n g f o r t h e c o n n e c t i o n e n t i t y v i a a c t i v e T h r e a d s i n N S E T h r e a d s
notificationEntities [ id ] ? . semaphore . wait ( )
logger . debug ( " NotificationService thread \( t , privacy : . private ) : receiveNtfMessages: entity \( id , privacy : . private ) proceeding after semaphore " )
Task {
// p r o c e s s a n y n o t i f i c a t i o n s " p o s t p o n e d " b y t h e p r e v i o u s i n s t a n c e
let completed = processDroppedNotifications ( ntfEntity , expectedMsg )
// R e q u e s t m e s s a g e s f r o m t h e s e r v e r f o r t h i s c o n n e c t i o n e n t i t y .
// I t t r i g g e r s e v e n t d e l i v e r y t o r e c e i v e M e s s a g e s l o o p ( s e e a b o v e ) .
if ! completed , let connMsgReq = notificationEntities [ id ] ? . connMsgReq ,
let rcvMsg = getConnNtfMessage ( connMsgReq : connMsgReq ) ,
rcvMsg . noMsg {
// i f s e r v e r r e t u r n s e r r o r o r " n o m e s s a g e " , d e l i v e r w h a t w e h a v e f o r t h i s c o n n e c t i o n e n t i t y .
finalizeEntity ( id ) // a l s o r e l e a s e s a n y w a i t i n g t h r e a d s f o r t h i s e n t i t y
}
}
return nil
}
} else { // n o e x p e c t e d m e s s a g e
notificationEntities [ id ] = ntfEntity
return nil
}
}
if ! connMsgReqs . isEmpty {
if let r = apiGetConnNtfMessages ( connMsgReqs : connMsgReqs ) {
logger . debug ( " NotificationService: receiveNtfMessages: apiGetConnNtfMessages count = \( r . count ) , expecting messages \( r . count { $0 != nil } ) " )
// R e q u e s t m e s s a g e s f o r a l l c o n n e c t i o n e n t i t i e s t h a t w e r e n o t u s e d b y o t h e r i n s t a n c e s .
// I t t r i g g e r s e v e n t d e l i v e r y t o r e c e i v e M e s s a g e s l o o p ( s e e a b o v e ) .
if ! msgReqs . isEmpty ,
let rcvMsgs = apiGetConnNtfMessages ( connMsgReqs : msgReqs . map { $0 . connMsgReq } ) {
for i in 0 . . < min ( msgReqs . count , rcvMsgs . count ) { // a s a n i t y c h e c k , A P I a l w a y s r e t u r n s t h e s a m e s i z e
if rcvMsgs [ i ] . noMsg {
// m a r k e n t i t y a s r e a d y i f t h e r e a r e n o m e s s a g e o n t h e s e r v e r ( o r o n e r r o r )
finalizeEntity ( msgReqs [ i ] . chatId )
}
}
return
}
} else if let dbStatus = dbStatus {
setServiceBestAttemptNtf ( createErrorNtf ( dbStatus , badgeCount ) )
}
}
// t r y t o d e l i v e r t h e b e s t a t t e m p t b e f o r e e x i t i n g
deliverBestAttemptNtf ( )
}
func addExpectedMessage ( ntfConn : NtfConn ) {
let expectedMsgId = ntfConn . expectedMsg_ ? . msgId
if let receiveEntityId = ntfConn . connEntity . id {
logger . debug ( " NotificationService: addExpectedMessage: expectedMsgId = \( expectedMsgId ? ? " nil " , privacy : . private ) " )
expectedMessages [ receiveEntityId ] = ExpectedMessage (
@ inline ( __always )
func mkNotificationEntity ( ntfConn : NtfConn ) -> NotificationEntity ? {
if let rcvEntityId = ntfConn . connEntity . id {
// d o n ' t r e c e i v e m e s s a g e s f o r m u t e d u s e r p r o f i l e
let expectedMsg : NtfMsgInfo ? = if ntfConn . user . showNotifications { ntfConn . expectedMsg_ } else { nil }
return NotificationEntity (
ntfConn : ntfConn ,
expectedMsgId : expectedMsgId ,
allowedGetNextAttempts : 3 ,
msgBestAttemptNtf : defaultBestAttemptNtf ( ntfConn ) ,
ready : ntfConn . expectedMsg_ = = nil , // s h o w d e f a u l t B e s t A t t e m p t N t f ( n t f C o n n ) i f t h e r e i s n o e x p e c t e d m e s s a g e
shouldProcessNtf : false ,
startedProcessingNewMsgs : false ,
semaphore : DispatchSemaphore ( value : 0 )
entityId : rcvEntityId ,
expectedMsg : expectedMsg ,
msgBestAttemptNtf : defaultBestAttemptNtf ( ntfConn )
)
}
return nil
}
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 )
// P r o c e s s e s n o t i f i c a t i o n s r e c e i v e d a n d p o s t p o n e d b y t h e p r e v i o u s N S E i n s t a n c e
func processDroppedNotifications ( _ ntfEntity : NotificationEntity , _ expectedMsg : NtfMsgInfo ) -> Bool {
var completed = false
while ! completed {
if let dropped = NSEThreads . shared . takeDroppedNtf ( ntfEntity ) {
completed = processReceivedNtf ( ntfEntity , expectedMsg , dropped . ntf )
} else {
break
}
}
if completed {
finalizeEntity ( ntfEntity . entityId )
} else {
notificationEntities [ ntfEntity . entityId ] ? . shouldProcessNtf = true
}
return completed
}
override func serviceExtensionTimeWillExpire ( ) {
@ -351,69 +517,70 @@ class NotificationService: UNNotificationServiceExtension {
deliverBestAttemptNtf ( urgent : true )
}
@ inline ( __always )
var expectingMoreMessages : Bool {
! expectedMessages . allSatisfy { $0 . value . ready }
notificationEntities . contains { $0 . value . expectedMsg != nil }
}
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
}
// p r o c e s s R e c e i v e d N t f r e t u r n s " c o m p l e t e d " - t r u e w h e n n o m o r e m e s s a g e s f o r t h e p a s s e d e n t i t y s h o u l d b e p r o c e s s e d b y t h e c u r r e n t N S E i n s t a n c e .
// T h i s i s u s e d t o c a l l f i n a l i z e E n t i t y ( i d ) a n d b y p r o c e s s D r o p p e d N o t i f i c a t i o n s t o d e c i d e i f f u r t h e r p r o c e s s i n g i s n e e d e d .
func processReceivedNtf ( _ ntfEntity : NotificationEntity , _ expectedMsg : NtfMsgInfo , _ ntf : NSENotificationData ) -> Bool {
let id = ntfEntity . entityId
if case let . msgInfo ( info ) = ntf {
if info . msgId = = expectedMessage . expectedMsgId {
if info . msgId = = expectedMsg . msgId {
// T h e m e s s a g e f o r t h i s i n s t a n c e i s p r o c e s s e d , n o m o r e e x p e c t e d , d e l i v e r .
logger . debug ( " NotificationService processNtf: msgInfo msgId = \( info . msgId , privacy : . private ) : expected " )
expectedMessages [ id ] ? . expectedMsgId = nil
if signalReady { entityReady ( id ) }
self . deliverBestAttemptNtf ( )
} else if let msgTs = info . msgTs_ , msgTs > expectedMsgTs {
return true
} else if let msgTs = info . msgTs_ , msgTs > expectedMsg . msgTs {
// O t h e r w i s e c h e c k t i m e s t a m p - i f i t i s a f t e r t h e c u r r e n t l y e x p e c t e d t i m e s t a m p , p r e s e r v e . m s g I n f o m a r k e r f o r t h e n e x t i n s t a n c e .
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 ( )
} else if ( expectedMessages [ id ] ? . allowedGetNextAttempts ? ? 0 ) > 0 , let connMsgReq = expectedMessages [ id ] ? . connMsgReq {
NSEThreads . shared . addDroppedNtf ( id , ntf )
return true
} else if ntfEntity . allowedGetNextAttempts > 0 , let connMsgReq = ntfEntity . connMsgReq {
// O t h e r w i s e t h i s i n s t a n c e e x p e c t s m o r e m e s s a g e s , a n d s t i l l h a s a l l o w e d a t t e m p t s -
// r e q u e s t m o r e m e s s a g e s w i t h g e t C o n n N t f M e s s a g e .
logger . debug ( " NotificationService processNtf: msgInfo msgId = \( info . msgId , privacy : . private ) : unexpected msgInfo, get next message " )
expectedMessages [ id ] ? . allowedGetNextAttempts -= 1
if let receivedMsg = getConnNtfMessage ( connMsgReq : connMsgReq ) {
logger . debug ( " NotificationService processNtf, on getConnNtfMessage: msgInfo msgId = \( info . msgId , privacy : . private ) , receivedMsg msgId = \( receivedMsg . msgId , privacy : . private ) " )
notificationEntities [ id ] ? . allowedGetNextAttempts -= 1
let receivedMsg = getConnNtfMessage ( connMsgReq : connMsgReq )
if case let . info ( msg ) = receivedMsg , let msg {
// S e r v e r d e l i v e r e d m e s s a g e , i t w i l l b e p r o c e s s e d i n t h e l o o p - s e e t h e c o m m e n t s i n r e c e i v e N t f M e s s a g e s .
logger . debug ( " NotificationService processNtf, on getConnNtfMessage: msgInfo msgId = \( info . msgId , privacy : . private ) , receivedMsg msgId = \( msg . msgId , privacy : . private ) " )
return false
} else {
// S e r v e r r e p o r t e d n o m e s s a g e s o r e r r o r , d e l i v e r w h a t w e h a v e .
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 true
}
} else {
// C u r r e n t i n s t a n c e n e e d s m o r e m e s s a g e s , b u t r a n o u t o f a t t e m p t s - d e l i v e r w h a t w e h a v e .
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 true
}
} else if expectedMessage . ntfConn . user . showNotifications {
} else if ntfEntity . ntfConn . user . showNotifications {
// T h i s i s t h e n o t i f i c a t i o n e v e n t f o r t h e u s e r w i t h e n a b l e d n o t i f i c a t i o n s .
logger . debug ( " NotificationService processNtf: setting best attempt " )
if ntf . notificationEvent != nil {
setBadgeCount ( )
}
let prevBestAttempt = expectedMessages [ id ] ? . msgBestAttemptNtf
if prevBestAttempt ? . callInvitation = = nil || ntf . callInvitation != nil {
expectedMessages [ id ] ? . msgBestAttemptNtf = ntf
// I f p r e v i o u s " b e s t a t t e m p t " i s n o t a c a l l , o r i f t h e c u r r e n t n o t i f i c a t i o n i s a c a l l , r e p l a c e b e s t a t t e m p t .
// N O T E : w e a r e d e l a y i n g i t u n t i l n o t i f i c a t i o n m a r k e r t o m a k e s u r e w e a r e n o t d e l i v e r i n g s t a l e c a l l s t h a t c a n ' t b e c o n n e c t e d .
// A b e t t e r l o g i c c o u l d b e t o c h e c k w h e t h e r w e h a v e a c a l l i n t h e b e s t a t t e m p t w h i l e p r o c e s s i n g . m s g I n f o m a r k e r a b o v e .
// I f t h e b e s t a t t e m p t i s a c a l l , a n d i t s m a r k e r i s r e c e i v e d , a n d t h e c a l l i s r e c e n t ( e . g . , t h e l a s t 3 0 s e c o n d s ) , i t w o u l d d e l i v e r a t o n c e ,
// i n s t e a d o f r e q u e s t i n g f u r t h e r m e s s a g e s .
if ntfEntity . msgBestAttemptNtf . callInvitation = = nil || ntf . callInvitation != nil {
notificationEntities [ id ] ? . msgBestAttemptNtf = ntf
} // o t h e r w i s e k e e p c a l l a s b e s t a t t e m p t
return false
} else {
NSEThreads . shared . droppedNotifications . append ( ( id , ntf ) )
if signalReady { entityReady ( id ) }
// W e s h o u l d n o t g e t t o t h i s b r a n c h , a s n o t i f i c a t i o n s a r e n o t d e l i v e r e d f o r m u t e d u s e r s .
return true
}
}
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 ( )
}
func finalizeEntity ( _ entityId : ChatId ) {
if let t = threadId { logger . debug ( " NotificationService thread \( t ) : entityReady: entity \( entityId ) " ) }
NSEThreads . shared . signalNextThread ( self , entityId )
deliverBestAttemptNtf ( )
}
func setBadgeCount ( ) {
@ -421,9 +588,10 @@ class NotificationService: UNNotificationServiceExtension {
ntfBadgeCountGroupDefault . set ( badgeCount )
}
@ inline ( __always )
func setServiceBestAttemptNtf ( _ ntf : UNMutableNotificationContent ) {
logger . debug ( " NotificationService.setServiceBestAttemptNtf " )
serviceBestAttemptNtf = . nse ( ntf )
serviceBestAttemptNtf = ntf
}
private func deliverBestAttemptNtf ( urgent : Bool = false ) {
@ -434,8 +602,8 @@ class NotificationService: UNNotificationServiceExtension {
}
logger . debug ( " NotificationService.deliverBestAttemptNtf " )
// s t o p p r o c e s s i n g o t h e r m e s s a g e s
for ( key , _ ) in expectedMessag es {
expectedMessag es[ key ] ? . shouldProcessNtf = false
for ( key , _ ) in notificationEntiti es {
notificationEntiti es[ key ] ? . shouldProcessNtf = false
}
let suspend : Bool
@ -449,22 +617,24 @@ class NotificationService: UNNotificationServiceExtension {
}
}
@ inline ( __always )
private func deliverCallkitOrNotification ( urgent : Bool , suspend : Bool = false , handler : @ escaping ( UNNotificationContent ) -> Void ) {
if useCallKit ( ) && expectedMessages . contains ( where : { $0 . value . msgBestAttemptNtf ? . callInvitation != nil } ) {
let callInv = notificationEntities . lazy . compactMap ( { $0 . value . msgBestAttemptNtf . callInvitation } ) . first
if callInv != nil && useCallKit ( ) {
logger . debug ( " NotificationService.deliverCallkitOrNotification: will suspend, callkit " )
if urgent {
// s u s p e n d i n g N S E e v e n t h o u g h t h e r e m a y b e o t h e r n o t i f i c a t i o n s
// t o a l l o w t h e a p p t o p r o c e s s c a l l k i t c a l l
if urgent {
suspendChat ( 0 )
deliverNotification ( handler : handler )
deliverNotification ( handler , callInv )
} else {
// su s p e n d i n g N S E w i t h d e l a y a n d d e l i v e r i n g a f t e r t h e s u s p e n s i o n
// wh e n n o t " u r g e n t " , su s p e n d i n g N S E w i t h d e l a y a n d d e l i v e r i n g a f t e r t h e s u s p e n s i o n
// b e c a u s e p u s h k i t n o t i f i c a t i o n m u s t b e p r o c e s s e d w i t h o u t d e l a y
// t o a v o i d a p p t e r m i n a t i o n
// t o a v o i d a p p t e r m i n a t i o n .
DispatchQueue . global ( ) . asyncAfter ( deadline : . now ( ) + fastNSESuspendSchedule . delay ) {
suspendChat ( fastNSESuspendSchedule . timeout )
DispatchQueue . global ( ) . asyncAfter ( deadline : . now ( ) + Double ( fastNSESuspendSchedule . timeout ) ) {
self . deliverNotification ( handler : handler )
self . deliverNotification ( handler , callInv )
}
}
}
@ -483,68 +653,71 @@ class NotificationService: UNNotificationServiceExtension {
}
}
}
deliverNotification ( handler : handler )
deliverNotification ( handler , callInv )
}
}
private func deliverNotification ( handler : @ escaping ( UNNotificationContent ) -> Void ) {
if serviceBestAttemptNtf != nil , let ntf = prepareNotification ( ) {
contentHandler = nil
private func deliverNotification ( _ handler : @ escaping ( UNNotificationContent ) -> Void , _ callInv : RcvCallInvitation ? ) {
if let serviceNtf = serviceBestAttemptNtf {
serviceBestAttemptNtf = nil
switch ntf {
case let . nse ( content ) :
content . badge = badgeCount as NSNumber
handler ( content )
case let . callkit ( invitation ) :
logger . debug ( " NotificationService reportNewIncomingVoIPPushPayload for \( invitation . contact . id ) " )
contentHandler = nil
if let callInv {
if useCallKit ( ) {
logger . debug ( " NotificationService reportNewIncomingVoIPPushPayload for \( callInv . contact . id ) " )
CXProvider . reportNewIncomingVoIPPushPayload ( [
" displayName " : invitation . contact . displayName ,
" contactId " : invitation . contact . id ,
" callUUID " : invitation . callUUID ? ? " " ,
" media " : invitation . callType . media . rawValue ,
" callTs " : invitation . callTs . timeIntervalSince1970
" displayName " : callInv . contact . displayName ,
" contactId " : callInv . contact . id ,
" callUUID " : callInv . callUUID ? ? " " ,
" media " : callInv . callType . media . rawValue ,
" callTs " : callInv . callTs . timeIntervalSince1970
] ) { error in
logger . debug ( " reportNewIncomingVoIPPushPayload result: \( error ) " )
handler ( error = = nil ? UNMutableNotificationContent ( ) : createCallInvitationNtf ( invitation , self . badgeCount ) )
handler ( error = = nil ? UNMutableNotificationContent ( ) : createCallInvitationNtf ( callInv , self . badgeCount ) )
}
case . empty :
handler ( UNMutableNotificationContent ( ) ) // u s e d t o m u t e n o t i f i c a t i o n s t h a t d i d n o t u n s u b s c r i b e y e t
} else {
handler ( createCallInvitationNtf ( callInv , badgeCount ) )
}
} else if notificationEntities . isEmpty {
handler ( serviceNtf )
} else {
handler ( prepareNotification ( ) )
}
}
}
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 {
logger . debug ( " NotificationService prepareNotification \( String ( describing : self . expectedMessages . map { $0 . key } ) ) " )
let ntfEvents = expectedMessages . compactMap { $0 . value . msgBestAttemptNtf ? . notificationEvent }
logger . debug ( " NotificationService prepareNotification \( ntfEvents . count ) " )
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 ) )
@ inline ( __always )
private func prepareNotification ( ) -> UNMutableNotificationContent {
// u n c o m m e n t l o c a l D i s p l a y N a m e i n C o n n e c t i o n E n t i t y
// l e t c o n n s = s e l f . n o t i f i c a t i o n E n t i t i e s . c o m p a c t M a p { $ 0 . v a l u e . n t f C o n n . c o n n E n t i t y . l o c a l D i s p l a y N a m e }
// l o g g e r . d e b u g ( " N o t i f i c a t i o n S e r v i c e p r e p a r e N o t i f i c a t i o n f o r \ ( S t r i n g ( d e s c r i b i n g : c o n n s ) ) " )
let ntfs = notificationEntities . compactMap { $0 . value . msgBestAttemptNtf . notificationEvent }
let newMsgNtfs = ntfs . compactMap ( { $0 . newMsgNtf } )
let useNtfs = if newMsgNtfs . isEmpty { ntfs } else { newMsgNtfs }
return createNtf ( useNtfs )
func createNtf ( _ ntfs : [ NSENotificationData ] ) -> UNMutableNotificationContent {
logger . debug ( " NotificationService prepareNotification: \( ntfs . count ) events " )
return switch ntfs . count {
case 0 : UNMutableNotificationContent ( ) // u s e d t o m u t e n o t i f i c a t i o n s t h a t d i d n o t u n s u b s c r i b e y e t
case 1 : ntfs [ 0 ] . notificationContent ( badgeCount )
default : createJointNtf ( ntfs )
}
}
}
private func createJointNtf ( _ ntfEvents : [ NSENotificationData ] ) -> UNMutableNotificationContent {
// N O T E : t h i s c a n b e i m p r o v e d w h e n t h e r e a r e t w o o r m o r e c o n n e c t i o n e n t i t y e v e n t s w h e n n o m e s s a g e s w e r e d e l i v e r e d .
// P o s s i b l y , i t i s b e t t e r t o p o s t p o n e t h i s i m p r o v e m e n t u n t i l m e s s a g e p r i o r i t y i s a d d e d t o p r e v e n t n o t i f i c a t i o n s i n m u t e d g r o u p s ,
// u n l e s s i t i s a m e n t i o n , a r e p l y o r s o m e o t h e r h i g h p r i o r i t y m e s s a g e m a r k e d f o r n o t i f i c a t i o n d e l i v e r y .
@ inline ( __always )
private func createJointNtf ( _ ntfs : [ 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 )
logger . debug ( " NotificationService.createJointNtf ntfs: \( ntfs . count ) " )
let ( userId , chatsNames ) = newMsgsChatsNames ( ntfs )
if ! chatsNames . isEmpty , let userId {
let body = if previewMode = = . hidden {
String . localizedStringWithFormat ( NSLocalizedString ( " From %d chat(s) " , comment : " notification body " ) , chatsNames . count )
} else {
body = String . localizedStringWithFormat ( NSLocalizedString ( " From: %@ " , comment : " notification body " ) , newMsgsChatsNamesStr ( uniqueC hatsNames) )
String . localizedStringWithFormat ( NSLocalizedString ( " From: %@ " , comment : " notification body " ) , newMsgsChatsNamesStr ( chatsNames ) )
}
return createNotification (
categoryIdentifier : ntfCategoryManyEvents ,
@ -557,24 +730,32 @@ class NotificationService: UNNotificationServiceExtension {
return createNotification (
categoryIdentifier : ntfCategoryManyEvents ,
title : NSLocalizedString ( " New events " , comment : " notification " ) ,
body : String . localizedStringWithFormat ( NSLocalizedString ( " %d new events " , comment : " notification body " ) , ntf Event s. count ) ,
body : String . localizedStringWithFormat ( NSLocalizedString ( " %d new events " , comment : " notification body " ) , ntf s. count ) ,
badgeCount : badgeCount
)
}
}
private func uniqueNewMsgsChatsNames ( _ newMsgsChats : [ ChatInfo ] ) -> [ String ] {
@ inline ( __always )
private func newMsgsChatsNames ( _ ntfs : [ NSENotificationData ] ) -> ( Int64 ? , [ String ] ) {
var seenChatIds = Set < ChatId > ( )
var uniqueChatsNames : [ String ] = [ ]
for chat in newMsgsChats {
var chatsNames : [ String ] = [ ]
var userId : Int64 ?
for ntf in ntfs {
switch ntf {
case let . messageReceived ( user , chat , _ ) :
if seenChatIds . isEmpty { userId = user . userId }
if ! seenChatIds . contains ( chat . id ) {
seenChatIds . insert ( chat . id )
uniqueChatsNames . append ( chat . chatViewName )
chatsNames . append ( chat . chatViewName )
}
default : ( )
}
}
return uniqueChatsNames
return ( userId , chatsNames )
}
@ inline ( __always )
private func newMsgsChatsNamesStr ( _ names : [ String ] ) -> String {
return switch names . count {
case 1 : names [ 0 ]
@ -593,9 +774,8 @@ class NSEChatState {
static let shared = NSEChatState ( )
private var value_ = NSEState . created
var value : NSEState {
value_
}
@ inline ( __always )
var value : NSEState { value_ }
func set ( _ state : NSEState ) {
nseStateGroupDefault . set ( state )
@ -603,7 +783,7 @@ class NSEChatState {
value_ = state
}
init( ) {
private init( ) {
// T h i s i s a l w a y s s e t t o . c r e a t e d s t a t e , a s i n c a s e p r e v i o u s s t a r t o f N S E c r a s h e d i n . a c t i v e s t a t e , i t i s s t o r e d c o r r e c t l y .
// O t h e r w i s e t h e a p p w i l l b e a c t i v a t i n g s l o w e r
set ( . created )
@ -803,8 +983,11 @@ func chatRecvMsg() async -> APIResult<NSEChatEvent>? {
}
private let isInChina = SKStorefront ( ) . countryCode = = " CHN "
@ inline ( __always )
private func useCallKit ( ) -> Bool { ! isInChina && callKitEnabledGroupDefault . get ( ) }
@ inline ( __always )
func receivedMsgNtf ( _ res : NSEChatEvent ) async -> ( String , NSENotificationData ) ? {
logger . debug ( " NotificationService receivedMsgNtf: \( res . responseType ) " )
switch res {
@ -851,12 +1034,10 @@ func receivedMsgNtf(_ res: NSEChatEvent) async -> (String, NSENotificationData)?
case . chatSuspended :
chatSuspended ( )
return nil
default :
logger . debug ( " NotificationService receivedMsgNtf ignored event: \( res . responseType ) " )
return nil
}
}
@ inline ( __always )
func updateNetCfg ( ) {
let newNetConfig = getNetCfg ( )
if newNetConfig != networkConfig {
@ -925,42 +1106,41 @@ func apiSetEncryptLocalFiles(_ enable: Bool) throws {
func apiGetNtfConns ( nonce : String , encNtfInfo : String ) -> [ NtfConn ] ? {
guard apiGetActiveUser ( ) != nil else {
logger . debug ( " no active user" )
logger . debug ( " NotificationService: no active user" )
return nil
}
let r : APIResult < NSEChatResponse > = sendSimpleXCmd ( NSEChatCommand . apiGetNtfConns ( nonce : nonce , encNtfInfo : encNtfInfo ) )
if case let . result ( . ntfConns ( ntfConns ) ) = r {
logger . debug ( " apiGetNtfConns response ntfConns: \( ntfConns . count ) ")
logger . debug ( " NotificationService apiGetNtfConns response ntfConns: \( ntfConns . count ) conections ")
return ntfConns
} else if case let . error ( error ) = r {
logger . debug ( " apiGetNtfMessage error response: \( String . init ( describing : error ) ) " )
logger . debug ( " NotificationService apiGetNtfMessage error response: \( String . init ( describing : error ) ) " )
} else {
logger . debug ( " apiGetNtfMessage ignored response: \( r . responseType ) \( String . init ( describing : r ) ) " )
logger . debug ( " NotificationService apiGetNtfMessage ignored response: \( r . responseType ) \( String . init ( describing : r ) ) " )
}
return nil
}
func apiGetConnNtfMessages ( connMsgReqs : [ ConnMsgReq ] ) -> [ NtfMsgInfo? ]? {
func apiGetConnNtfMessages ( connMsgReqs : [ ConnMsgReq ] ) -> [ Rcv NtfMsgInfo ]? {
guard apiGetActiveUser ( ) != nil else {
logger . debug ( " no active user " )
return nil
}
logger . debug ( " apiGetConnNtfMessages command: \( NSEChatCommand . apiGetConnNtfMessages ( connMsgReqs : connMsgReqs ) . cmdString ) " )
// l o g g e r . d e b u g ( " N o t i f i c a t i o n S e r v i c e a p i G e t C o n n N t f M e s s a g e s c o m m a n d : \ ( N S E C h a t C o m m a n d . a p i G e t C o n n N t f M e s s a g e s ( c o n n M s g R e q s : c o n n M s g R e q s ) . c m d S t r i n g ) " )
logger . debug ( " NotificationService apiGetConnNtfMessages requests: \( connMsgReqs . count ) " )
let r : APIResult < NSEChatResponse > = sendSimpleXCmd ( NSEChatCommand . apiGetConnNtfMessages ( connMsgReqs : connMsgReqs ) )
if case let . result ( . connNtfMessages ( receivedMsgs ) ) = r {
logger . debug ( " apiGetConnNtfMessages response receivedMsgs: total \( receivedMsgs . count ) , expecting messages \( receivedMsgs . count { $0 != nil } ) " )
return receivedMsgs
if case let . result ( . connNtfMessages ( msgs ) ) = r {
// l o g g e r . d e b u g ( " N o t i f i c a t i o n S e r v i c e a p i G e t C o n n N t f M e s s a g e s r e s p o n s e s : \ ( S t r i n g ( d e s c r i b i n g : m s g s ) ) " )
logger . debug ( " NotificationService apiGetConnNtfMessages responses: total \( msgs . count ) , expecting messages \( msgs . count { ! $0 . noMsg } ) , errors \( msgs . count { $0 . isError } ) " )
return msgs
}
logger . debug ( " apiGetConnNtfMessages error: \( responseError ( r . unexpected ) ) " )
logger . debug ( " NotificationService apiGetConnNtfMessages error: \( responseError ( r . unexpected ) ) " )
return nil
}
func getConnNtfMessage ( connMsgReq : ConnMsgReq ) -> NtfMsgInfo ? {
let r_ = apiGetConnNtfMessages ( connMsgReqs : [ connMsgReq ] )
if let r = r_ , let receivedMsg = r . count = = 1 ? r . first : nil {
return receivedMsg
}
return nil
func getConnNtfMessage ( connMsgReq : ConnMsgReq ) -> RcvNtfMsgInfo ? {
let r = apiGetConnNtfMessages ( connMsgReqs : [ connMsgReq ] )
return if let r , r . count > 0 { r [ 0 ] } else { nil }
}
func apiReceiveFile ( fileId : Int64 , encrypted : Bool , inline : Bool ? = nil ) -> AChatItem ? {
@ -1021,4 +1201,3 @@ func defaultBestAttemptNtf(_ ntfConn: NtfConn) -> NSENotificationData {
}
}
}