Signal-iOS/Signal/src/NotificationActionHandler.swift
2021-04-29 18:01:18 -07:00

194 lines
9.0 KiB
Swift

//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import Foundation
import PromiseKit
class NotificationActionHandler: Dependencies {
static let shared: NotificationActionHandler = NotificationActionHandler()
func answerCall(userInfo: [AnyHashable: Any]) throws -> Promise<Void> {
guard let localCallIdString = userInfo[AppNotificationUserInfoKey.localCallId] as? String else {
throw OWSAssertionError("localCallIdString was unexpectedly nil")
}
guard let localCallId = UUID(uuidString: localCallIdString) else {
throw OWSAssertionError("unable to build localCallId. localCallIdString: \(localCallIdString)")
}
individualCallUIAdapter.answerCall(localId: localCallId)
return Promise.value(())
}
func callBack(userInfo: [AnyHashable: Any]) throws -> Promise<Void> {
let uuidString = userInfo[AppNotificationUserInfoKey.callBackUuid] as? String
let phoneNumber = userInfo[AppNotificationUserInfoKey.callBackPhoneNumber] as? String
let address = SignalServiceAddress(uuidString: uuidString, phoneNumber: phoneNumber)
guard address.isValid else {
throw OWSAssertionError("Missing or invalid address.")
}
individualCallUIAdapter.startAndShowOutgoingCall(address: address, hasLocalVideo: false)
return Promise.value(())
}
func declineCall(userInfo: [AnyHashable: Any]) throws -> Promise<Void> {
guard let localCallIdString = userInfo[AppNotificationUserInfoKey.localCallId] as? String else {
throw OWSAssertionError("localCallIdString was unexpectedly nil")
}
guard let localCallId = UUID(uuidString: localCallIdString) else {
throw OWSAssertionError("unable to build localCallId. localCallIdString: \(localCallIdString)")
}
individualCallUIAdapter.localHangupCall(localId: localCallId)
return Promise.value(())
}
func markAsRead(userInfo: [AnyHashable: Any]) throws -> Promise<Void> {
return firstly {
self.notificationMessage(forUserInfo: userInfo)
}.then(on: .global()) { (notificationMessage: NotificationMessage) in
self.markMessageAsRead(notificationMessage: notificationMessage)
}
}
func reply(userInfo: [AnyHashable: Any], replyText: String) throws -> Promise<Void> {
return firstly { () -> Promise<NotificationMessage> in
self.notificationMessage(forUserInfo: userInfo)
}.then(on: .global()) { (notificationMessage: NotificationMessage) -> Promise<Void> in
let thread = notificationMessage.thread
let interaction = notificationMessage.interaction
guard nil != interaction as? TSIncomingMessage else {
throw OWSAssertionError("Unexpected interaction type.")
}
return firstly(on: .global()) { () -> Promise<Void> in
self.databaseStorage.write { transaction in
ThreadUtil.sendMessageNonDurablyPromise(body: MessageBody(text: replyText, ranges: .empty),
thread: thread,
transaction: transaction)
}
}.recover(on: .global()) { error -> Promise<Void> in
Logger.warn("Failed to send reply message from notification with error: \(error)")
self.notificationPresenter.notifyForFailedSend(inThread: thread)
throw error
}.then(on: .global()) { () -> Promise<Void> in
self.markMessageAsRead(notificationMessage: notificationMessage)
}
}
}
func showThread(userInfo: [AnyHashable: Any]) throws -> Promise<Void> {
return firstly { () -> Promise<NotificationMessage> in
self.notificationMessage(forUserInfo: userInfo)
}.done(on: .main) { notificationMessage in
self.showThread(notificationMessage: notificationMessage)
}
}
private func showThread(notificationMessage: NotificationMessage) {
// 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.
signalApp.presentConversationAndScrollToFirstUnreadMessage(
forThreadId: notificationMessage.thread.uniqueId,
animated: UIApplication.shared.applicationState == .active
)
}
func reactWithThumbsUp(userInfo: [AnyHashable: Any]) throws -> Promise<Void> {
return firstly { () -> Promise<NotificationMessage> in
self.notificationMessage(forUserInfo: userInfo)
}.then(on: .global()) { (notificationMessage: NotificationMessage) -> Promise<Void> in
let thread = notificationMessage.thread
let interaction = notificationMessage.interaction
guard let incomingMessage = interaction as? TSIncomingMessage else {
throw OWSAssertionError("Unexpected interaction type.")
}
return firstly(on: .global()) { () -> Promise<Void> in
self.databaseStorage.write { transaction in
ReactionManager.localUserReactedWithNonDurableSend(to: incomingMessage, emoji: "👍", isRemoving: false, transaction: transaction)
}
}.recover(on: .global()) { error -> Promise<Void> in
Logger.warn("Failed to send reply message from notification with error: \(error)")
self.notificationPresenter.notifyForFailedSend(inThread: thread)
throw error
}.then(on: .global()) { () -> Promise<Void> in
self.markMessageAsRead(notificationMessage: notificationMessage)
}
}
}
func showCallLobby(userInfo: [AnyHashable: Any]) throws -> Promise<Void> {
return firstly { () -> Promise<NotificationMessage> in
self.notificationMessage(forUserInfo: userInfo)
}.done(on: .main) { notificationMessage in
let thread = notificationMessage.thread
let currentCall = Self.callService.currentCall
if currentCall?.thread.uniqueId == thread.uniqueId {
OWSWindowManager.shared.returnToCallView()
} else if let thread = thread as? TSGroupThread, currentCall == nil {
GroupCallViewController.presentLobby(thread: thread)
} else {
// If currentCall is non-nil, we can't join a call anyway, fallback to showing the thread.
// Individual calls don't have a lobby, just show the thread.
return self.showThread(notificationMessage: notificationMessage)
}
}
}
private struct NotificationMessage {
let thread: TSThread
let interaction: TSInteraction?
let hasPendingMessageRequest: Bool
}
private func notificationMessage(forUserInfo userInfo: [AnyHashable: Any]) -> Promise<NotificationMessage> {
firstly(on: .global()) { () throws -> NotificationMessage in
guard let threadId = userInfo[AppNotificationUserInfoKey.threadId] as? String else {
throw OWSAssertionError("threadId was unexpectedly nil")
}
let messageId = userInfo[AppNotificationUserInfoKey.messageId] as? String
return try self.databaseStorage.read { (transaction) throws -> NotificationMessage in
guard let thread = TSThread.anyFetch(uniqueId: threadId, transaction: transaction) else {
throw OWSAssertionError("unable to find thread with id: \(threadId)")
}
let interaction: TSInteraction?
if let messageId = messageId {
interaction = TSInteraction.anyFetch(uniqueId: messageId, transaction: transaction)
} else {
interaction = nil
}
let hasPendingMessageRequest = thread.hasPendingMessageRequest(transaction: transaction.unwrapGrdbRead)
return NotificationMessage(
thread: thread,
interaction: interaction,
hasPendingMessageRequest: hasPendingMessageRequest
)
}
}
}
private func markMessageAsRead(notificationMessage: NotificationMessage) -> Promise<Void> {
guard let interaction = notificationMessage.interaction else {
return Promise(error: OWSAssertionError("missing interaction"))
}
let (promise, resolver) = Promise<Void>.pending()
self.receiptManager.markAsReadLocally(beforeSortId: interaction.sortId,
thread: notificationMessage.thread,
hasPendingMessageRequest: notificationMessage.hasPendingMessageRequest) {
resolver.fulfill(())
}
return promise
}
}