Alert user of safety number changes in group call
- Present SafetyNumberConfirmationSheet for all group members before joining call from lobby. - Present SafetyNumberConfirmationSheet for newly joined participants with a safety number mismatch - If the call is minimized or backgrounded, present a notification.
This commit is contained in:
parent
de37b1dd4a
commit
eaef20b780
@ -107,6 +107,8 @@ protocol CallAudioServiceDelegate: class {
|
||||
}
|
||||
|
||||
private let routePicker = AVRoutePickerView()
|
||||
|
||||
@discardableResult
|
||||
public func presentRoutePicker() -> Bool {
|
||||
guard let routeButton = routePicker.subviews.first(where: { $0 is UIButton }) as? UIButton else {
|
||||
owsFailDebug("Failed to find subview to present route picker, falling back to old system")
|
||||
|
||||
@ -786,6 +786,15 @@ extension CallService: CallManagerDelegate {
|
||||
AssertIsOnMainThread()
|
||||
Logger.info("shouldSendCallMessage")
|
||||
|
||||
// It's unlikely that this would ever have more than one call. But technically
|
||||
// we don't know which call this message is on behalf of. So we assume it's every
|
||||
// call with a participant with recipientUuid
|
||||
let relevantCalls = calls.filter { (call: SignalCall) -> Bool in
|
||||
call.participantAddresses
|
||||
.compactMap { $0.uuid }
|
||||
.contains(recipientUuid)
|
||||
}
|
||||
|
||||
databaseStorage.write(.promise) { transaction in
|
||||
TSContactThread.getOrCreateThread(
|
||||
withContactAddress: SignalServiceAddress(uuid: recipientUuid),
|
||||
@ -804,10 +813,10 @@ extension CallService: CallManagerDelegate {
|
||||
}.done { _ in
|
||||
// TODO: Tell RingRTC we succeeded in sending the message. API TBD
|
||||
}.catch { error in
|
||||
// Did the recipients safety number change?
|
||||
// Is the user blocked?
|
||||
if error.isNetworkFailureOrTimeout {
|
||||
Logger.warn("Failed to send opaque message \(error)")
|
||||
} else if error.isUntrustedIdentityError {
|
||||
relevantCalls.forEach { $0.publishSendFailureUntrustedParticipantIdentity() }
|
||||
} else {
|
||||
Logger.error("Failed to send opaque message \(error)")
|
||||
}
|
||||
@ -1067,3 +1076,10 @@ extension CallService: CallManagerDelegate {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Error {
|
||||
var isUntrustedIdentityError: Bool {
|
||||
let nsError = self as NSError
|
||||
return nsError.domain == OWSSignalServiceKitErrorDomain && nsError.code == OWSErrorCode.untrustedIdentity.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +19,10 @@ public protocol CallObserver: class {
|
||||
func groupCallRequestMembershipProof(_ call: SignalCall)
|
||||
func groupCallRequestGroupMembers(_ call: SignalCall)
|
||||
func groupCallEnded(_ call: SignalCall, reason: GroupCallEndReason)
|
||||
|
||||
/// Invoked if a call message failed to send because of a safety number change
|
||||
/// UI observing call state may choose to alert the user (e.g. presenting a SafetyNumberConfirmationSheet)
|
||||
func callMessageSendFailedUntrustedIdentity(_ call: SignalCall)
|
||||
}
|
||||
|
||||
public extension CallObserver {
|
||||
@ -34,6 +38,8 @@ public extension CallObserver {
|
||||
func groupCallRequestMembershipProof(_ call: SignalCall) {}
|
||||
func groupCallRequestGroupMembers(_ call: SignalCall) {}
|
||||
func groupCallEnded(_ call: SignalCall, reason: GroupCallEndReason) {}
|
||||
|
||||
func callMessageSendFailedUntrustedIdentity(_ call: SignalCall) {}
|
||||
}
|
||||
|
||||
@objc
|
||||
@ -104,6 +110,15 @@ public class SignalCall: NSObject, CallManagerCallReference {
|
||||
case messageSendFailure(underlyingError: Error)
|
||||
}
|
||||
|
||||
var participantAddresses: [SignalServiceAddress] {
|
||||
switch mode {
|
||||
case .group(let call):
|
||||
return call.remoteDeviceStates.values.map { $0.address }
|
||||
case .individual(let call):
|
||||
return [call.remoteAddress]
|
||||
}
|
||||
}
|
||||
|
||||
init(groupCall: GroupCall, groupThread: TSGroupThread) {
|
||||
mode = .group(groupCall)
|
||||
audioActivity = AudioActivity(
|
||||
@ -215,6 +230,10 @@ public class SignalCall: NSObject, CallManagerCallReference {
|
||||
observers = []
|
||||
}
|
||||
|
||||
public func publishSendFailureUntrustedParticipantIdentity() {
|
||||
observers.elements.forEach { $0.callMessageSendFailedUntrustedIdentity(self) }
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
// This method should only be called when the call state is "connected".
|
||||
|
||||
@ -39,6 +39,7 @@ class GroupCallViewController: UIViewController {
|
||||
var shouldRemoteVideoControlsBeHidden = false {
|
||||
didSet { updateCallUI() }
|
||||
}
|
||||
var hasUnresolvedSafetyNumberMismatch = false
|
||||
|
||||
private static let keyValueStore = SDSKeyValueStore(collection: "GroupCallViewController")
|
||||
|
||||
@ -199,6 +200,12 @@ class GroupCallViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
if hasUnresolvedSafetyNumberMismatch {
|
||||
resolveSafetyNumberMismatch()
|
||||
}
|
||||
}
|
||||
|
||||
private var hasOverflowMembers: Bool { videoGrid.maxItems < groupCall.remoteDeviceStates.count }
|
||||
|
||||
private func updateScrollViewFrames(size: CGSize? = nil, controlsAreHidden: Bool) {
|
||||
@ -492,8 +499,97 @@ extension GroupCallViewController: CallViewControllerWindowReference {
|
||||
self.updateCallUI()
|
||||
splitViewSnapshot.removeFromSuperview()
|
||||
pipSnapshot.removeFromSuperview()
|
||||
|
||||
if self.hasUnresolvedSafetyNumberMismatch {
|
||||
self.resolveSafetyNumberMismatch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resolveSafetyNumberMismatch() {
|
||||
if !isCallMinimized, CurrentAppContext().isAppForegroundAndActive() {
|
||||
presentSafetyNumberChangeSheetIfNecessary { [weak self] success in
|
||||
guard let self = self else { return }
|
||||
if success {
|
||||
self.groupCall.resendMediaKeys()
|
||||
self.hasUnresolvedSafetyNumberMismatch = false
|
||||
} else {
|
||||
self.dismissCall()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
AppEnvironment.shared.notificationPresenter.notifyForGroupCallSafetyNumberChange(inThread: call.thread)
|
||||
}
|
||||
}
|
||||
|
||||
func presentSafetyNumberChangeSheetIfNecessary(completion: @escaping (Bool) -> Void) {
|
||||
let localDeviceHasNotJoined = groupCall.localDeviceState.joinState == .notJoined
|
||||
let currentParticipantAddresses = groupCall.remoteDeviceStates.map { $0.value.address }
|
||||
|
||||
// If we haven't joined the call yet, we want to alert for all members of the group
|
||||
// If we are in the call, we only care about safety numbers for the active call participants
|
||||
let addressesToAlert = call.thread.recipientAddresses.filter { memberAddress in
|
||||
let isUntrusted = OWSIdentityManager.shared().untrustedIdentityForSending(to: memberAddress) != nil
|
||||
let isMemberInCall = currentParticipantAddresses.contains(memberAddress)
|
||||
|
||||
// We want to alert for safety number changes of all members if we haven't joined yet
|
||||
// If we're already in the call, we only care about active call participants
|
||||
return isUntrusted && (isMemberInCall || localDeviceHasNotJoined)
|
||||
}
|
||||
|
||||
// There are no unverified addresses that we're currently concerned about. No need to show a sheet
|
||||
guard addressesToAlert.count > 0 else { completion(true); return }
|
||||
|
||||
let startCallString = NSLocalizedString("GROUP_CALL_START_BUTTON", comment: "Button to start a group call")
|
||||
let joinCallString = NSLocalizedString("GROUP_CALL_JOIN_BUTTON", comment: "Button to join an ongoing group call")
|
||||
let continueCallString = NSLocalizedString("GROUP_CALL_CONTINUE_BUTTON", comment: "Button to continue an ongoing group call")
|
||||
let leaveCallString = NSLocalizedString("GROUP_CALL_LEAVE_BUTTON", comment: "Button to leave a group call")
|
||||
let cancelString = CommonStrings.cancelButton
|
||||
|
||||
let approveText: String
|
||||
let denyText: String
|
||||
if localDeviceHasNotJoined {
|
||||
let deviceCount = call.groupCall.peekInfo?.deviceCount ?? 0
|
||||
approveText = deviceCount > 0 ? joinCallString : startCallString
|
||||
denyText = cancelString
|
||||
} else {
|
||||
approveText = continueCallString
|
||||
denyText = leaveCallString
|
||||
}
|
||||
|
||||
let sheet = SafetyNumberConfirmationSheet(
|
||||
addressesToConfirm: addressesToAlert,
|
||||
confirmationText: approveText,
|
||||
cancelText: denyText,
|
||||
theme: .translucentDark) { didApprove in
|
||||
|
||||
if didApprove {
|
||||
SDSDatabaseStorage.shared.asyncWrite { writeTx in
|
||||
let identityManager = OWSIdentityManager.shared()
|
||||
for address in addressesToAlert {
|
||||
guard let identityKey = identityManager.identityKey(for: address, transaction: writeTx) else { return }
|
||||
let currentState = identityManager.verificationState(for: address, transaction: writeTx)
|
||||
let newState = (currentState == .noLongerVerified) ? .default : currentState
|
||||
|
||||
identityManager.setVerificationState(newState,
|
||||
identityKey: identityKey,
|
||||
address: address,
|
||||
isUserInitiatedChange: true,
|
||||
transaction: writeTx)
|
||||
}
|
||||
} completion: {
|
||||
completion(true)
|
||||
}
|
||||
|
||||
} else {
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
sheet.allowsDismissal = false
|
||||
sheet.confirmAction.button.titleLabel?.font = UIFont.ows_dynamicTypeBody.ows_semibold
|
||||
sheet.cancelAction.button.titleLabel?.font = UIFont.ows_dynamicTypeBody
|
||||
present(sheet, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension GroupCallViewController: CallObserver {
|
||||
@ -560,6 +656,16 @@ extension GroupCallViewController: CallObserver {
|
||||
))
|
||||
presentActionSheet(actionSheet)
|
||||
}
|
||||
|
||||
func callMessageSendFailedUntrustedIdentity(_ call: SignalCall) {
|
||||
AssertIsOnMainThread()
|
||||
guard call == self.call else { return owsFailDebug("Unexpected call \(call)") }
|
||||
|
||||
if !hasUnresolvedSafetyNumberMismatch {
|
||||
hasUnresolvedSafetyNumberMismatch = true
|
||||
resolveSafetyNumberMismatch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension GroupCallViewController: CallControlsDelegate {
|
||||
@ -603,7 +709,14 @@ extension GroupCallViewController: CallControlsDelegate {
|
||||
}
|
||||
|
||||
func didPressJoin(sender: UIButton) {
|
||||
callService.joinGroupCallIfNecessary(call)
|
||||
presentSafetyNumberChangeSheetIfNecessary { [weak self] success in
|
||||
guard let self = self else { return }
|
||||
if success {
|
||||
self.callService.joinGroupCallIfNecessary(self.call)
|
||||
} else {
|
||||
self.dismissCall()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -113,23 +113,27 @@ class NotificationActionHandler {
|
||||
return firstly { () -> Promise<NotificationMessage> in
|
||||
self.notificationMessage(forUserInfo: userInfo)
|
||||
}.done(on: .main) { notificationMessage in
|
||||
let thread = notificationMessage.thread
|
||||
let currentCall = AppEnvironment.shared.callService.currentCall
|
||||
let isGroupCallMessage = notificationMessage.interaction is OWSGroupCallMessage
|
||||
self.showThread(notificationMessage: notificationMessage)
|
||||
}
|
||||
}
|
||||
|
||||
if isGroupCallMessage, currentCall?.thread.uniqueId == thread.uniqueId {
|
||||
OWSWindowManager.shared.returnToCallView()
|
||||
} else if let thread = thread as? TSGroupThread, isGroupCallMessage, currentCall == nil {
|
||||
GroupCallViewController.presentLobby(thread: thread)
|
||||
} else {
|
||||
// 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.
|
||||
self.signalApp.presentConversationAndScrollToFirstUnreadMessage(
|
||||
forThreadId: thread.uniqueId,
|
||||
animated: UIApplication.shared.applicationState == .active
|
||||
)
|
||||
}
|
||||
private func showThread(notificationMessage: NotificationMessage) {
|
||||
let thread = notificationMessage.thread
|
||||
let currentCall = AppEnvironment.shared.callService.currentCall
|
||||
let isGroupCallMessage = notificationMessage.interaction is OWSGroupCallMessage
|
||||
|
||||
if isGroupCallMessage, currentCall?.thread.uniqueId == thread.uniqueId {
|
||||
OWSWindowManager.shared.returnToCallView()
|
||||
} else if let thread = thread as? TSGroupThread, isGroupCallMessage, currentCall == nil {
|
||||
GroupCallViewController.presentLobby(thread: thread)
|
||||
} else {
|
||||
// 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.
|
||||
self.signalApp.presentConversationAndScrollToFirstUnreadMessage(
|
||||
forThreadId: thread.uniqueId,
|
||||
animated: UIApplication.shared.applicationState == .active
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,6 +161,25 @@ class NotificationActionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
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 = AppEnvironment.shared.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?
|
||||
|
||||
@ -31,21 +31,24 @@ public class UserNotificationActionHandler: NSObject {
|
||||
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
|
||||
let action: AppNotificationAction
|
||||
|
||||
switch response.actionIdentifier {
|
||||
case UNNotificationDefaultActionIdentifier:
|
||||
Logger.debug("default action")
|
||||
return try actionHandler.showThread(userInfo: userInfo)
|
||||
let defaultActionString = userInfo[AppNotificationUserInfoKey.defaultAction] as? String
|
||||
let defaultAction = defaultActionString.flatMap { AppNotificationAction(rawValue: $0) }
|
||||
action = defaultAction ?? .showThread
|
||||
case UNNotificationDismissActionIdentifier:
|
||||
// TODO - mark as read?
|
||||
Logger.debug("dismissed notification")
|
||||
return Promise.value(())
|
||||
default:
|
||||
// proceed
|
||||
break
|
||||
}
|
||||
|
||||
guard let action = UserNotificationConfig.action(identifier: response.actionIdentifier) else {
|
||||
throw OWSAssertionError("unable to find action for actionIdentifier: \(response.actionIdentifier)")
|
||||
if let responseAction = UserNotificationConfig.action(identifier: response.actionIdentifier) {
|
||||
action = responseAction
|
||||
} else {
|
||||
throw OWSAssertionError("unable to find action for actionIdentifier: \(response.actionIdentifier)")
|
||||
}
|
||||
}
|
||||
|
||||
switch action {
|
||||
@ -67,6 +70,8 @@ public class UserNotificationActionHandler: NSObject {
|
||||
return try actionHandler.showThread(userInfo: userInfo)
|
||||
case .reactWithThumbsUp:
|
||||
return try actionHandler.reactWithThumbsUp(userInfo: userInfo)
|
||||
case .showCallLobby:
|
||||
return try actionHandler.showCallLobby(userInfo: userInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1531,6 +1531,9 @@
|
||||
/* String displayed in group call grid cell when a user is blocked. Embeds {user's name} */
|
||||
"GROUP_CALL_BLOCKED_USER_FORMAT" = "%@ is blocked";
|
||||
|
||||
/* Button to continue an ongoing group call */
|
||||
"GROUP_CALL_CONTINUE_BUTTON" = "Continue Call";
|
||||
|
||||
/* Text in conversation view for a group call that has since ended */
|
||||
"GROUP_CALL_ENDED_MESSAGE" = "The group call has ended";
|
||||
|
||||
@ -1549,6 +1552,9 @@
|
||||
/* Button to join an ongoing group call */
|
||||
"GROUP_CALL_JOIN_BUTTON" = "Join Call";
|
||||
|
||||
/* Button to leave a group call */
|
||||
"GROUP_CALL_LEAVE_BUTTON" = "Leave Call";
|
||||
|
||||
/* String indicating how many people are current in the call */
|
||||
"GROUP_CALL_MANY_IN_THIS_CALL_FORMAT" = "In this call · %ld people";
|
||||
|
||||
@ -1597,6 +1603,9 @@
|
||||
/* Text indicating that the user has lost their connection to the call and we are reconnecting. */
|
||||
"GROUP_CALL_RECONNECTING" = "Reconnecting";
|
||||
|
||||
/* notification body when a group call participant joins with an untrusted safety number */
|
||||
"GROUP_CALL_SAFETY_NUMBER_CHANGE_BODY" = "Someone has joined this call with a safety number that has changed.";
|
||||
|
||||
/* Text in conversation view for a group call that someone started. We don't know who */
|
||||
"GROUP_CALL_SOMEONE_STARTED_MESSAGE" = "Someone started a group call";
|
||||
|
||||
|
||||
@ -36,7 +36,7 @@ public enum AppNotificationCategory: CaseIterable {
|
||||
case grdbMigration
|
||||
}
|
||||
|
||||
public enum AppNotificationAction: CaseIterable {
|
||||
public enum AppNotificationAction: String, CaseIterable {
|
||||
case answerCall
|
||||
case callBack
|
||||
case declineCall
|
||||
@ -44,6 +44,7 @@ public enum AppNotificationAction: CaseIterable {
|
||||
case reply
|
||||
case showThread
|
||||
case reactWithThumbsUp
|
||||
case showCallLobby
|
||||
}
|
||||
|
||||
public struct AppNotificationUserInfoKey {
|
||||
@ -53,6 +54,7 @@ public struct AppNotificationUserInfoKey {
|
||||
public static let callBackUuid = "Signal.AppNotificationsUserInfoKey.callBackUuid"
|
||||
public static let callBackPhoneNumber = "Signal.AppNotificationsUserInfoKey.callBackPhoneNumber"
|
||||
public static let localCallId = "Signal.AppNotificationsUserInfoKey.localCallId"
|
||||
public static let defaultAction = "Signal.AppNotificationsUserInfoKey.defaultAction"
|
||||
}
|
||||
|
||||
extension AppNotificationCategory {
|
||||
@ -139,6 +141,8 @@ extension AppNotificationAction {
|
||||
return "Signal.AppNotifications.Action.showThread"
|
||||
case .reactWithThumbsUp:
|
||||
return "Signal.AppNotifications.Action.reactWithThumbsUp"
|
||||
case .showCallLobby:
|
||||
return "Signal.AppNotifications.Action.showCallLobby"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -617,6 +621,33 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
public func notifyForGroupCallSafetyNumberChange(inThread thread: TSThread) {
|
||||
let notificationTitle: String?
|
||||
switch previewType {
|
||||
case .noNameNoPreview:
|
||||
notificationTitle = nil
|
||||
case .nameNoPreview, .namePreview:
|
||||
notificationTitle = contactsManager.displayNameWithSneakyTransaction(thread: thread)
|
||||
}
|
||||
|
||||
let notificationBody = NotificationStrings.groupCallSafetyNumberChangeBody
|
||||
let threadId = thread.uniqueId
|
||||
let userInfo: [String: Any] = [
|
||||
AppNotificationUserInfoKey.threadId: threadId,
|
||||
AppNotificationUserInfoKey.defaultAction: AppNotificationAction.showCallLobby.rawValue
|
||||
]
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let sound = self.requestSound(thread: thread)
|
||||
self.adaptee.notify(category: .infoOrErrorMessage,
|
||||
title: notificationTitle,
|
||||
body: notificationBody,
|
||||
threadIdentifier: nil, // show ungrouped
|
||||
userInfo: userInfo,
|
||||
sound: sound)
|
||||
}
|
||||
}
|
||||
|
||||
public func notifyUser(for errorMessage: TSErrorMessage, thread: TSThread, transaction: SDSAnyWriteTransaction) {
|
||||
switch errorMessage.errorType {
|
||||
case .noSession,
|
||||
@ -660,10 +691,14 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
||||
notificationBody = previewableInteraction.previewText(transaction: transaction)
|
||||
}
|
||||
|
||||
let isGroupCallMessage = previewableInteraction is OWSGroupCallMessage
|
||||
let preferredDefaultAction: AppNotificationAction = isGroupCallMessage ? .showCallLobby : .showThread
|
||||
|
||||
let threadId = thread.uniqueId
|
||||
let userInfo = [
|
||||
AppNotificationUserInfoKey.threadId: threadId,
|
||||
AppNotificationUserInfoKey.messageId: previewableInteraction.uniqueId
|
||||
AppNotificationUserInfoKey.messageId: previewableInteraction.uniqueId,
|
||||
AppNotificationUserInfoKey.defaultAction: preferredDefaultAction.rawValue
|
||||
]
|
||||
|
||||
transaction.addAsyncCompletion {
|
||||
|
||||
@ -14,7 +14,7 @@ public class UserNotificationConfig {
|
||||
}
|
||||
|
||||
class func notificationActions(for category: AppNotificationCategory) -> [UNNotificationAction] {
|
||||
return category.actions.map { notificationAction($0) }
|
||||
return category.actions.compactMap { notificationAction($0) }
|
||||
}
|
||||
|
||||
class func notificationCategory(_ category: AppNotificationCategory) -> UNNotificationCategory {
|
||||
@ -24,7 +24,7 @@ public class UserNotificationConfig {
|
||||
options: [])
|
||||
}
|
||||
|
||||
class func notificationAction(_ action: AppNotificationAction) -> UNNotificationAction {
|
||||
class func notificationAction(_ action: AppNotificationAction) -> UNNotificationAction? {
|
||||
switch action {
|
||||
case .answerCall:
|
||||
return UNNotificationAction(identifier: action.identifier,
|
||||
@ -56,11 +56,16 @@ public class UserNotificationConfig {
|
||||
return UNNotificationAction(identifier: action.identifier,
|
||||
title: MessageStrings.reactWithThumbsUpNotificationAction,
|
||||
options: [])
|
||||
|
||||
case .showCallLobby:
|
||||
// Currently, .showCallLobby is only used as a default action.
|
||||
owsFailDebug("Show call lobby not supported as a UNNotificationAction")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public class func action(identifier: String) -> AppNotificationAction? {
|
||||
return AppNotificationAction.allCases.first { notificationAction($0).identifier == identifier }
|
||||
return AppNotificationAction.allCases.first { notificationAction($0)?.identifier == identifier }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -171,6 +171,10 @@ public class NotificationStrings: NSObject {
|
||||
@objc
|
||||
static public let failedToSendBody = NSLocalizedString("SEND_FAILED_NOTIFICATION_BODY", comment: "notification body")
|
||||
|
||||
@objc
|
||||
static public let groupCallSafetyNumberChangeBody = NSLocalizedString("GROUP_CALL_SAFETY_NUMBER_CHANGE_BODY",
|
||||
comment: "notification body when a group call participant joins with an untrusted safety number")
|
||||
|
||||
@objc
|
||||
static public let incomingReactionFormat = NSLocalizedString("REACTION_INCOMING_NOTIFICATION_BODY_FORMAT",
|
||||
comment: "notification body. Embeds {{reaction emoji}}")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user