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.
258 lines
11 KiB
Swift
258 lines
11 KiB
Swift
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import Foundation
|
|
import Combine
|
|
import GRDB
|
|
import SignalUtilitiesKit
|
|
import SessionSnodeKit
|
|
import SessionMessagingKit
|
|
import SessionUtilitiesKit
|
|
|
|
// MARK: - Singleton
|
|
|
|
public extension Singleton {
|
|
static let notificationActionHandler: SingletonConfig<NotificationActionHandler> = Dependencies.create(
|
|
identifier: "notificationActionHandler",
|
|
createInstance: { dependencies in NotificationActionHandler(using: dependencies) }
|
|
)
|
|
}
|
|
|
|
// MARK: - NotificationActionHandler
|
|
|
|
public class NotificationActionHandler {
|
|
private let dependencies: Dependencies
|
|
|
|
// MARK: - Initialization
|
|
|
|
init(using dependencies: Dependencies) {
|
|
self.dependencies = dependencies
|
|
}
|
|
|
|
// MARK: - Handling
|
|
|
|
func handleNotificationResponse(
|
|
_ response: UNNotificationResponse,
|
|
completionHandler: @escaping () -> Void
|
|
) {
|
|
Log.assertOnMainThread()
|
|
handleNotificationResponse(response)
|
|
.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()
|
|
Log.error("[NotificationActionHandler] An error occured handling a notification response: \(error)")
|
|
}
|
|
},
|
|
receiveValue: { _ in completionHandler() }
|
|
)
|
|
}
|
|
|
|
func handleNotificationResponse(_ response: UNNotificationResponse) -> AnyPublisher<Void, Error> {
|
|
Log.assertOnMainThread()
|
|
assert(dependencies[singleton: .appReadiness].isAppReady)
|
|
|
|
let userInfo: [AnyHashable: Any] = response.notification.request.content.userInfo
|
|
let applicationState: UIApplication.State = UIApplication.shared.applicationState
|
|
|
|
switch response.actionIdentifier {
|
|
case UNNotificationDefaultActionIdentifier:
|
|
Log.debug("[NotificationActionHandler] Default action")
|
|
return showThread(userInfo: userInfo)
|
|
.setFailureType(to: Error.self)
|
|
.eraseToAnyPublisher()
|
|
|
|
case UNNotificationDismissActionIdentifier:
|
|
// TODO - mark as read?
|
|
Log.debug("[NotificationActionHandler] 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)
|
|
|
|
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
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Actions
|
|
|
|
func markAsRead(userInfo: [AnyHashable: Any]) -> 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)
|
|
}
|
|
|
|
func reply(
|
|
userInfo: [AnyHashable: Any],
|
|
replyText: String,
|
|
applicationState: UIApplication.State
|
|
) -> 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 { [dependencies] db -> Network.PreparedRequest<Void> in
|
|
let sentTimestampMs: Int64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs()
|
|
let destinationDisappearingMessagesConfiguration: DisappearingMessagesConfiguration? = try? DisappearingMessagesConfiguration
|
|
.filter(id: threadId)
|
|
.filter(DisappearingMessagesConfiguration.Columns.isEnabled == true)
|
|
.fetchOne(db)
|
|
let interaction: Interaction = try Interaction(
|
|
threadId: threadId,
|
|
threadVariant: thread.variant,
|
|
authorId: dependencies[cache: .general].sessionId.hexString,
|
|
variant: .standardOutgoing,
|
|
body: replyText,
|
|
timestampMs: sentTimestampMs,
|
|
hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: replyText, using: dependencies),
|
|
expiresInSeconds: destinationDisappearingMessagesConfiguration?.durationSeconds,
|
|
expiresStartedAtMs: (destinationDisappearingMessagesConfiguration?.type == .disappearAfterSend ? Double(sentTimestampMs) : nil),
|
|
using: dependencies
|
|
).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
|
|
),
|
|
using: dependencies
|
|
)
|
|
|
|
return try MessageSender.preparedSend(
|
|
db,
|
|
interaction: interaction,
|
|
fileIds: [],
|
|
threadId: threadId,
|
|
threadVariant: thread.variant,
|
|
using: dependencies
|
|
)
|
|
}
|
|
.flatMap { [dependencies] request in request.send(using: dependencies) }
|
|
.map { _ in () }
|
|
.handleEvents(
|
|
receiveCompletion: { [dependencies] result in
|
|
switch result {
|
|
case .finished: break
|
|
case .failure:
|
|
dependencies[singleton: .storage].read { db in
|
|
dependencies[singleton: .notificationsManager].notifyForFailedSend(
|
|
db,
|
|
in: thread,
|
|
applicationState: applicationState
|
|
)
|
|
}
|
|
}
|
|
}
|
|
)
|
|
.eraseToAnyPublisher()
|
|
}
|
|
|
|
func showThread(userInfo: [AnyHashable: Any]) -> 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.
|
|
dependencies[singleton: .app].presentConversationCreatingIfNeeded(
|
|
for: threadId,
|
|
variant: threadVariant,
|
|
action: .none,
|
|
dismissing: nil,
|
|
animated: (UIApplication.shared.applicationState == .active)
|
|
)
|
|
|
|
return Just(()).eraseToAnyPublisher()
|
|
}
|
|
|
|
func showHomeVC() -> AnyPublisher<Void, Never> {
|
|
dependencies[singleton: .app].showHomeView()
|
|
return Just(()).eraseToAnyPublisher()
|
|
}
|
|
|
|
private func markAsRead(threadId: String) -> AnyPublisher<Void, Error> {
|
|
return dependencies[singleton: .storage]
|
|
.writePublisher { [dependencies] 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
|
|
),
|
|
using: dependencies
|
|
)
|
|
}
|
|
.eraseToAnyPublisher()
|
|
}
|
|
}
|