@ -20,10 +20,6 @@ public enum GroupInviteMemberJob: JobExecutor {
public static var requiresThreadId : Bool = true
public static var requiresInteractionId : Bool = false
private static let notificationDebounceDuration : DispatchQueue . SchedulerTimeType . Stride = . milliseconds ( 1500 )
private static var notifyFailurePublisher : AnyPublisher < Void , Never > ?
private static let notifyFailureTrigger : PassthroughSubject < ( ) , Never > = PassthroughSubject ( )
public static func run (
_ job : Job ,
queue : DispatchQueue ,
@ -127,11 +123,9 @@ public enum GroupInviteMemberJob: JobExecutor {
}
// N o t i f y a b o u t t h e f a i l u r e
GroupInviteMemberJob . notifyOfFailure (
groupId : threadId ,
memberId : details . memberSessionIdHexString ,
using : dependencies
)
dependencies . mutate ( cache : . groupInviteMemberJob ) { cache in
cache . addInviteFailure ( groupId : threadId , memberId : details . memberSessionIdHexString )
}
// R e g i s t e r t h e f a i l u r e
switch error {
@ -199,88 +193,122 @@ public enum GroupInviteMemberJob: JobExecutor {
. localizedFormatted ( baseFont : ToastController . font )
}
}
}
// MARK: - G r o u p I n v i t e M e m b e r J o b C a c h e
public extension GroupInviteMemberJob {
struct InviteFailure : Hashable {
let groupId : String
let memberId : String
}
private static func notifyOfFailure ( groupId : String , memberId : String , using dependencies : Dependencies ) {
dependencies . mutate ( cache : . groupInviteMemberJob ) { cache in
cache . failedMemberIds . insert ( memberId )
class Cache : GroupInviteMemberJobCacheType {
private static let notificationDebounceDuration : DispatchQueue . SchedulerTimeType . Stride = . milliseconds ( 3000 )
private let dependencies : Dependencies
private let inviteFailedNotificationTrigger : PassthroughSubject < ( ) , Never > = PassthroughSubject ( )
private var disposables : Set < AnyCancellable > = Set ( )
public private ( set ) var inviteFailures : Set < InviteFailure > = [ ]
// MARK: - I n i t i a l i a t i o n
init ( using dependencies : Dependencies ) {
self . dependencies = dependencies
setupInviteFailureListener ( )
}
// / T h i s m e t h o d c a n b e t r i g g e r e d b y e a c h i n d i v i d u a l i n v i t a t i o n f a i l u r e s o w e w a n t t o t h r o t t l e t h e u p d a t e s t o 2 5 0 m s s o t h a t w e c a n g r o u p f a i l u r e s
// / a n d s h o w a s i n g l e t o a s t
if notifyFailurePublisher = = nil {
notifyFailurePublisher = notifyFailureTrigger
. debounce ( for : notificationDebounceDuration , scheduler : DispatchQueue . global ( qos : . userInitiated ) )
. handleEvents (
receiveOutput : { [ dependencies ] _ in
let failedIds : [ String ] = dependencies . mutate ( cache : . groupInviteMemberJob ) { cache in
let result : Set < String > = cache . failedMemberIds
cache . failedMemberIds . removeAll ( )
return Array ( result )
}
// D o n ' t d o a n y t h i n g i f t h e r e a r e n o ' f a i l e d I d s ' v a l u e s o r w e c a n ' t g e t a w i n d o w
guard
! failedIds . isEmpty ,
let mainWindow : UIWindow = dependencies [ singleton : . appContext ] . mainWindow
else { return }
typealias FetchedData = ( groupName : String , profileInfo : [ String : Profile ] )
let data : FetchedData = dependencies [ singleton : . storage ]
. read { db in
(
try ClosedGroup
. filter ( id : groupId )
. select ( . name )
. asRequest ( of : String . self )
. fetchOne ( db ) ,
try Profile . filter ( ids : failedIds ) . fetchAll ( db )
)
}
. map { maybeName , profiles -> FetchedData in
(
( maybeName ? ? " groupUnknown " . localized ( ) ) ,
profiles . reduce ( into : [ : ] ) { result , next in result [ next . id ] = next }
)
}
. defaulting ( to : ( " groupUnknown " . localized ( ) , [ : ] ) )
let message : NSAttributedString = failureMessage (
groupName : data . groupName ,
memberIds : failedIds ,
profileInfo : data . profileInfo
)
// MARK: - F u n c t i o n s
public func addInviteFailure ( groupId : String , memberId : String ) {
print ( " [RAWR] Add failure for: \( memberId ) " )
inviteFailures . insert ( InviteFailure ( groupId : groupId , memberId : memberId ) )
inviteFailedNotificationTrigger . send ( ( ) )
}
public func clearPendingFailures ( for groupId : String ) {
inviteFailures = inviteFailures . filter { $0 . groupId != groupId }
}
// MARK: - I n t e r n a l F u n c t i o n s
private func setupInviteFailureListener ( ) {
inviteFailedNotificationTrigger
. subscribe ( on : DispatchQueue . global ( qos : . userInitiated ) , using : dependencies )
. debounce (
for : Cache . notificationDebounceDuration ,
scheduler : DispatchQueue . global ( qos : . userInitiated )
)
. map { [ dependencies ] _ -> ( failedInvites : Set < InviteFailure > , groupId : String ) in
dependencies . mutate ( cache : . groupInviteMemberJob ) { cache in
guard let targetGroupId : String = cache . inviteFailures . first ? . groupId else { return ( [ ] , " " ) }
DispatchQueue . main . async {
let toastController : ToastController = ToastController (
text : message ,
background : . backgroundSecondary
)
toastController . presentToastView ( fromBottomOfView : mainWindow , inset : Values . largeSpacing )
let result : Set < InviteFailure > = cache . inviteFailures . filter { $0 . groupId = = targetGroupId }
cache . clearPendingFailures ( for : targetGroupId )
return ( result , targetGroupId )
}
}
. filter { failedInvites , _ in ! failedInvites . isEmpty }
. setFailureType ( to : Error . self )
. flatMapStorageReadPublisher ( using : dependencies , value : { db , data -> ( maybeName : String ? , failedMemberIds : [ String ] , profiles : [ Profile ] ) in
let failedMemberIds : [ String ] = data . failedInvites . map { $0 . memberId }
return (
try ClosedGroup
. filter ( id : data . groupId )
. select ( . name )
. asRequest ( of : String . self )
. fetchOne ( db ) ,
failedMemberIds ,
try Profile . filter ( ids : failedMemberIds ) . fetchAll ( db )
)
} )
. map { maybeName , failedMemberIds , profiles -> ( groupName : String , failedIds : [ String ] , profileMap : [ String : Profile ] ) in
let profileMap : [ String : Profile ] = profiles . reduce ( into : [ : ] ) { result , next in
result [ next . id ] = next
}
let sortedFailedMemberIds : [ String ] = failedMemberIds . sorted { lhs , rhs in
// S o r t b y n a m e , f o l l o w e d b y i d i f n a m e s a r e n ' t p r e s e n t
switch ( profileMap [ lhs ] ? . displayName ( for : . group ) , profileMap [ rhs ] ? . displayName ( for : . group ) ) {
case ( . some ( let lhsName ) , . some ( let rhsName ) ) : return lhsName < rhsName
case ( . some , . none ) : return true
case ( . none , . some ) : return false
case ( . none , . none ) : return lhs < rhs
}
}
)
. map { _ in ( ) }
. eraseToAnyPublisher ( )
notifyFailurePublisher ? . sinkUntilComplete ( )
return (
( maybeName ? ? " groupUnknown " . localized ( ) ) ,
sortedFailedMemberIds ,
profileMap
)
}
. catch { _ in Just ( ( " " , [ ] , [ : ] ) ) . eraseToAnyPublisher ( ) }
. filter { _ , failedIds , _ in ! failedIds . isEmpty }
. receive ( on : DispatchQueue . main , using : dependencies )
. sink ( receiveValue : { [ dependencies ] groupName , failedIds , profileMap in
guard let mainWindow : UIWindow = dependencies [ singleton : . appContext ] . mainWindow else { return }
let toastController : ToastController = ToastController (
text : GroupInviteMemberJob . failureMessage (
groupName : groupName ,
memberIds : failedIds ,
profileInfo : profileMap
) ,
background : . backgroundSecondary
)
toastController . presentToastView ( fromBottomOfView : mainWindow , inset : Values . largeSpacing )
} )
. store ( in : & disposables )
}
notifyFailureTrigger . send ( ( ) )
}
}
// MARK: - G r o u p I n v i t e M e m b e r J o b C a c h e
public extension GroupInviteMemberJob {
class Cache : GroupInviteMemberJobCacheType {
public var failedMemberIds : Set < String > = [ ]
}
}
public extension Cache {
static let groupInviteMemberJob : CacheConfig < GroupInviteMemberJobCacheType , GroupInviteMemberJobImmutableCacheType > = Dependencies . create (
identifier : " groupInviteMemberJob " ,
createInstance : { _ in GroupInviteMemberJob . Cache ( ) } ,
createInstance : { dependencies in GroupInviteMemberJob . Cache ( using : dependencies ) } ,
mutableInstance : { $0 } ,
immutableInstance : { $0 }
)
@ -290,11 +318,14 @@ public extension Cache {
// / T h i s i s a r e a d - o n l y v e r s i o n o f t h e C a c h e d e s i g n e d t o a v o i d u n i n t e n t i o n a l l y m u t a t i n g t h e i n s t a n c e i n a n o n - t h r e a d - s a f e w a y
public protocol GroupInviteMemberJobImmutableCacheType : ImmutableCacheType {
var failedMemberIds: Set < String > { get }
var inviteFailures: Set < GroupInviteMemberJob . InviteFailure > { get }
}
public protocol GroupInviteMemberJobCacheType : GroupInviteMemberJobImmutableCacheType , MutableCacheType {
var failedMemberIds : Set < String > { get set }
var inviteFailures : Set < GroupInviteMemberJob . InviteFailure > { get }
func addInviteFailure ( groupId : String , memberId : String )
func clearPendingFailures ( for groupId : String )
}
// MARK: - G r o u p I n v i t e M e m b e r J o b . D e t a i l s