mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			255 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			255 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Swift
		
	
| // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
 | |
| 
 | |
| import Foundation
 | |
| import Combine
 | |
| import GRDB
 | |
| import SignalCoreKit
 | |
| import SignalUtilitiesKit
 | |
| import SessionSnodeKit
 | |
| import SessionMessagingKit
 | |
| import SessionUtilitiesKit
 | |
| 
 | |
| // MARK: - Singleton
 | |
| 
 | |
| public extension Singleton {
 | |
|     static let notificationActionHandler: SingletonConfig<NotificationActionHandler> = Dependencies.create(
 | |
|         identifier: "notificationActionHandler",
 | |
|         createInstance: { _ in NotificationActionHandler() }
 | |
|     )
 | |
| }
 | |
| 
 | |
| // MARK: - NotificationActionHandler
 | |
| 
 | |
| public class NotificationActionHandler {
 | |
|     // MARK: - Handling
 | |
|     
 | |
|     func handleNotificationResponse(
 | |
|         _ response: UNNotificationResponse,
 | |
|         completionHandler: @escaping () -> Void,
 | |
|         using dependencies: Dependencies
 | |
|     ) {
 | |
|         AssertIsOnMainThread()
 | |
|         handleNotificationResponse(response, using: dependencies)
 | |
|             .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies)
 | |
|             .receive(on: DispatchQueue.main, using: dependencies)
 | |
|             .sinkUntilComplete(
 | |
|                 receiveCompletion: { result in
 | |
|                     switch result {
 | |
|                         case .finished: break
 | |
|                         case .failure(let error):
 | |
|                             completionHandler()
 | |
|                             owsFailDebug("error: \(error)")
 | |
|                             Logger.error("error: \(error)")
 | |
|                     }
 | |
|                 },
 | |
|                 receiveValue: { _ in completionHandler() }
 | |
|             )
 | |
|     }
 | |
| 
 | |
|     func handleNotificationResponse(
 | |
|         _ response: UNNotificationResponse,
 | |
|         using dependencies: Dependencies
 | |
|     ) -> AnyPublisher<Void, Error> {
 | |
|         AssertIsOnMainThread()
 | |
|         assert(AppReadiness.isAppReady())
 | |
| 
 | |
|         let userInfo: [AnyHashable: Any] = response.notification.request.content.userInfo
 | |
|         let applicationState: UIApplication.State = UIApplication.shared.applicationState
 | |
| 
 | |
|         switch response.actionIdentifier {
 | |
|             case UNNotificationDefaultActionIdentifier:
 | |
|                 Logger.debug("default action")
 | |
|                 return showThread(userInfo: userInfo, using: dependencies)
 | |
|                     .setFailureType(to: Error.self)
 | |
|                     .eraseToAnyPublisher()
 | |
|                 
 | |
|             case UNNotificationDismissActionIdentifier:
 | |
|                 // TODO - mark as read?
 | |
|                 Logger.debug("dismissed notification")
 | |
|                 return Just(())
 | |
|                     .setFailureType(to: Error.self)
 | |
|                     .eraseToAnyPublisher()
 | |
|                 
 | |
|             default:
 | |
|                 // proceed
 | |
|                 break
 | |
|         }
 | |
| 
 | |
|         guard let action = UserNotificationConfig.action(identifier: response.actionIdentifier) else {
 | |
|             return Fail(error: NotificationError.failDebug("unable to find action for actionIdentifier: \(response.actionIdentifier)"))
 | |
|                 .eraseToAnyPublisher()
 | |
|         }
 | |
| 
 | |
|         switch action {
 | |
|             case .markAsRead: return markAsRead(userInfo: userInfo, using: dependencies)
 | |
|                 
 | |
|             case .reply:
 | |
|                 guard let textInputResponse = response as? UNTextInputNotificationResponse else {
 | |
|                     return Fail(error: NotificationError.failDebug("response had unexpected type: \(response)"))
 | |
|                         .eraseToAnyPublisher()
 | |
|                 }
 | |
| 
 | |
|                 return reply(
 | |
|                     userInfo: userInfo,
 | |
|                     replyText: textInputResponse.userText,
 | |
|                     applicationState: applicationState,
 | |
|                     using: dependencies
 | |
|                 )
 | |
|                     
 | |
|             case .showThread:
 | |
|                 return showThread(userInfo: userInfo, using: dependencies)
 | |
|                     .setFailureType(to: Error.self)
 | |
|                     .eraseToAnyPublisher()
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // MARK: - Actions
 | |
| 
 | |
|     func markAsRead(userInfo: [AnyHashable: Any], using dependencies: Dependencies) -> AnyPublisher<Void, Error> {
 | |
|         guard let threadId: String = userInfo[AppNotificationUserInfoKey.threadId] as? String else {
 | |
|             return Fail(error: NotificationError.failDebug("threadId was unexpectedly nil"))
 | |
|                 .eraseToAnyPublisher()
 | |
|         }
 | |
|         
 | |
|         guard dependencies[singleton: .storage].read({ db in try SessionThread.exists(db, id: threadId) }) == true else {
 | |
|             return Fail(error: NotificationError.failDebug("unable to find thread with id: \(threadId)"))
 | |
|                 .eraseToAnyPublisher()
 | |
|         }
 | |
| 
 | |
|         return markAsRead(threadId: threadId, using: dependencies)
 | |
|     }
 | |
| 
 | |
|     func reply(
 | |
|         userInfo: [AnyHashable: Any],
 | |
|         replyText: String,
 | |
|         applicationState: UIApplication.State,
 | |
|         using dependencies: Dependencies
 | |
|     ) -> AnyPublisher<Void, Error> {
 | |
|         guard let threadId = userInfo[AppNotificationUserInfoKey.threadId] as? String else {
 | |
|             return Fail<Void, Error>(error: NotificationError.failDebug("threadId was unexpectedly nil"))
 | |
|                 .eraseToAnyPublisher()
 | |
|         }
 | |
|         
 | |
|         guard let thread: SessionThread = dependencies[singleton: .storage].read({ db in try SessionThread.fetchOne(db, id: threadId) }) else {
 | |
|             return Fail<Void, Error>(error: NotificationError.failDebug("unable to find thread with id: \(threadId)"))
 | |
|                 .eraseToAnyPublisher()
 | |
|         }
 | |
|         
 | |
|         return dependencies[singleton: .storage]
 | |
|             .writePublisher { db -> HTTP.PreparedRequest<Void> in
 | |
|                 let interaction: Interaction = try Interaction(
 | |
|                     threadId: threadId,
 | |
|                     authorId: getUserSessionId(db, using: dependencies).hexString,
 | |
|                     variant: .standardOutgoing,
 | |
|                     body: replyText,
 | |
|                     timestampMs: SnodeAPI.currentOffsetTimestampMs(using: dependencies),
 | |
|                     hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: replyText)
 | |
|                 ).inserted(db)
 | |
|                 
 | |
|                 try Interaction.markAsRead(
 | |
|                     db,
 | |
|                     interactionId: interaction.id,
 | |
|                     threadId: threadId,
 | |
|                     threadVariant: thread.variant,
 | |
|                     includingOlder: true,
 | |
|                     trySendReadReceipt: try SessionThread.canSendReadReceipt(
 | |
|                         db,
 | |
|                         threadId: threadId,
 | |
|                         threadVariant: thread.variant
 | |
|                     ),
 | |
|                     using: dependencies
 | |
|                 )
 | |
|                 
 | |
|                 return try MessageSender.preparedSend(
 | |
|                     db,
 | |
|                     interaction: interaction,
 | |
|                     fileIds: [],
 | |
|                     threadId: threadId,
 | |
|                     threadVariant: thread.variant,
 | |
|                     using: dependencies
 | |
|                 )
 | |
|             }
 | |
|             .flatMap { $0.send(using: dependencies) }
 | |
|             .map { _ in () }
 | |
|             .handleEvents(
 | |
|                 receiveCompletion: { result in
 | |
|                     switch result {
 | |
|                         case .finished: break
 | |
|                         case .failure:
 | |
|                             dependencies[singleton: .storage].read { [dependencies] db in
 | |
|                                 dependencies[singleton: .notificationsManager].notifyForFailedSend(
 | |
|                                     db,
 | |
|                                     in: thread,
 | |
|                                     applicationState: applicationState
 | |
|                                 )
 | |
|                             }
 | |
|                     }
 | |
|                 }
 | |
|             )
 | |
|             .eraseToAnyPublisher()
 | |
|     }
 | |
| 
 | |
|     func showThread(userInfo: [AnyHashable: Any], using dependencies: Dependencies) -> AnyPublisher<Void, Never> {
 | |
|         guard
 | |
|             let threadId = userInfo[AppNotificationUserInfoKey.threadId] as? String,
 | |
|             let threadVariantRaw = userInfo[AppNotificationUserInfoKey.threadVariantRaw] as? Int,
 | |
|             let threadVariant: SessionThread.Variant = SessionThread.Variant(rawValue: threadVariantRaw)
 | |
|         else { return showHomeVC() }
 | |
| 
 | |
|         // If this happens when the the app is not, visible we skip the animation so the thread
 | |
|         // can be visible to the user immediately upon opening the app, rather than having to watch
 | |
|         // it animate in from the homescreen.
 | |
|         SessionApp.presentConversationCreatingIfNeeded(
 | |
|             for: threadId,
 | |
|             variant: threadVariant,
 | |
|             dismissing: nil,
 | |
|             animated: (UIApplication.shared.applicationState == .active),
 | |
|             using: dependencies
 | |
|         )
 | |
|         
 | |
|         return Just(()).eraseToAnyPublisher()
 | |
|     }
 | |
|     
 | |
|     func showHomeVC() -> AnyPublisher<Void, Never> {
 | |
|         SessionApp.showHomeView()
 | |
|         return Just(()).eraseToAnyPublisher()
 | |
|     }
 | |
|     
 | |
|     private func markAsRead(
 | |
|         threadId: String,
 | |
|         using dependencies: Dependencies
 | |
|     ) -> AnyPublisher<Void, Error> {
 | |
|         return dependencies[singleton: .storage]
 | |
|             .writePublisher { db in
 | |
|                 guard
 | |
|                     let threadVariant: SessionThread.Variant = try SessionThread
 | |
|                         .filter(id: threadId)
 | |
|                         .select(.variant)
 | |
|                         .asRequest(of: SessionThread.Variant.self)
 | |
|                         .fetchOne(db),
 | |
|                     let lastInteractionId: Int64 = try Interaction
 | |
|                         .select(.id)
 | |
|                         .filter(Interaction.Columns.threadId == threadId)
 | |
|                         .order(Interaction.Columns.timestampMs.desc)
 | |
|                         .asRequest(of: Int64.self)
 | |
|                         .fetchOne(db)
 | |
|                 else { throw NotificationError.failDebug("unable to required thread info: \(threadId)") }
 | |
| 
 | |
|                 try Interaction.markAsRead(
 | |
|                     db,
 | |
|                     interactionId: lastInteractionId,
 | |
|                     threadId: threadId,
 | |
|                     threadVariant: threadVariant,
 | |
|                     includingOlder: true,
 | |
|                     trySendReadReceipt: try SessionThread.canSendReadReceipt(
 | |
|                         db,
 | |
|                         threadId: threadId,
 | |
|                         threadVariant: threadVariant
 | |
|                     ),
 | |
|                     using: dependencies
 | |
|                 )
 | |
|             }
 | |
|             .eraseToAnyPublisher()
 | |
|     }
 | |
| }
 |