From ab6301c3e909bb6421bd55e697a71cfe08b07d2a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 6 Jul 2022 11:52:10 +0100 Subject: [PATCH] ios: notification preview mode, show connection entity notification (#781) * ios: notification preview mode, show connection entity notification * prepare connection entity notification as best attempt --- apps/ios/Shared/Model/ChatModel.swift | 2 +- apps/ios/Shared/Model/NtfManager.swift | 15 +--- .../UserSettings/NotificationsView.swift | 14 ++- .../ios/SimpleX NSE/NotificationService.swift | 29 +++---- apps/ios/SimpleXChat/APITypes.swift | 4 +- apps/ios/SimpleXChat/AppGroup.swift | 7 ++ apps/ios/SimpleXChat/ChatTypes.swift | 7 ++ apps/ios/SimpleXChat/Notifications.swift | 86 ++++++++++++++++--- 8 files changed, 115 insertions(+), 49 deletions(-) diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 7ebad52128..92170ecbb3 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -33,7 +33,7 @@ final class ChatModel: ObservableObject { @Published var savedToken: DeviceToken? @Published var tokenStatus: NtfTknStatus? @Published var notificationMode = NotificationsMode.off - @Published var notificationPreview: NotificationPreviewMode? = .message + @Published var notificationPreview: NotificationPreviewMode? = ntfPreviewModeGroupDefault.get() // current WebRTC call @Published var callInvitations: Dictionary = [:] @Published var activeCall: Call? diff --git a/apps/ios/Shared/Model/NtfManager.swift b/apps/ios/Shared/Model/NtfManager.swift index 307f1f8a61..de78a60780 100644 --- a/apps/ios/Shared/Model/NtfManager.swift +++ b/apps/ios/Shared/Model/NtfManager.swift @@ -136,12 +136,11 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: NSLocalizedString("Incoming call", comment: "notification") ), - // TODO remove UNNotificationCategory( - identifier: ntfCategoryCheckingMessages, + identifier: ntfCategoryConnectionEvent, actions: [], intentIdentifiers: [], - hiddenPreviewsBodyPlaceholder: NSLocalizedString("Checking new messages...", comment: "notification") + hiddenPreviewsBodyPlaceholder: NSLocalizedString("SimpleX encrypted message or connection event", comment: "notification") ) ]) } @@ -189,16 +188,6 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { addNotification(createCallInvitationNtf(invitation)) } - // TODO remove - func notifyCheckingMessages() { - logger.debug("NtfManager.notifyCheckingMessages") - let content = createNotification( - categoryIdentifier: ntfCategoryCheckingMessages, - title: NSLocalizedString("Checking new messages...", comment: "notification") - ) - addNotification(content) - } - private func addNotification(_ content: UNMutableNotificationContent) { if !granted { return } let trigger = UNTimeIntervalNotificationTrigger(timeInterval: ntfTimeInterval, repeats: false) diff --git a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift index 4b8acc4c05..4e4001b24e 100644 --- a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift +++ b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift @@ -55,9 +55,19 @@ struct NotificationsView: View { NavigationLink { List { Section { - SelectionListView(list: NotificationPreviewMode.values, selection: $m.notificationPreview) + SelectionListView(list: NotificationPreviewMode.values, selection: $m.notificationPreview) { previewMode in + ntfPreviewModeGroupDefault.set(previewMode) + m.notificationPreview = previewMode + } } footer: { - + VStack(alignment: .leading, spacing: 1) { + Text("You can set lock screen notification preview via settings.") + Button("Open Settings") { + DispatchQueue.main.async { + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) + } + } + } } } .navigationTitle("Show preview") diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 2fa61c57df..c937786210 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -45,23 +45,14 @@ class NotificationService: UNNotificationServiceExtension { let encNtfInfo = ntfData["message"] as? String, let _ = startChat() { if let ntfMsgInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) { - if let content = receiveMessageForNotification() { - contentHandler(content) - } else if let connEntity = ntfMsgInfo.connEntity { - switch connEntity { - case let .rcvDirectMsgConnection(_, contact): - () - case let .rcvGroupMsgConnection(_, groupInfo, groupMember): - () - case let .sndFileConnection(_, sndFileTransfer): - () - case let .rcvFileConnection(_, rcvFileTransfer): - () - case let .userContactConnection(_, userContact): - () - } - contentHandler(request.content) - } + if let connEntity = ntfMsgInfo.connEntity { + bestAttemptContent = createConnectionEventNtf(connEntity) + } + if let content = receiveMessageForNotification() { + contentHandler(content) + } else if let content = bestAttemptContent { + contentHandler(content) + } } } } @@ -70,8 +61,8 @@ class NotificationService: UNNotificationServiceExtension { logger.debug("NotificationService.serviceExtensionTimeWillExpire") // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. - if let contentHandler = self.contentHandler, let bestAttemptContent = self.bestAttemptContent { - contentHandler(bestAttemptContent) + if let contentHandler = self.contentHandler, let content = bestAttemptContent { + contentHandler(content) } } } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 6fa970ae62..b2622a539e 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -452,8 +452,8 @@ public enum NotificationPreviewMode: String, SelectableItem { public var label: LocalizedStringKey { switch self { case .hidden: return "Hidden" - case .contact: return "Contact" - case .message: return "Message" + case .contact: return "Contact name" + case .message: return "Message text" } } diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index b71d192969..a1ee992750 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -12,6 +12,7 @@ import SwiftUI let GROUP_DEFAULT_APP_STATE = "appState" let GROUP_DEFAULT_DB_CONTAINER = "dbContainer" public let GROUP_DEFAULT_CHAT_LAST_START = "chatLastStart" +let GROUP_DEFAULT_NTF_PREVIEW_MODE = "ntfPreviewMode" let APP_GROUP_NAME = "group.chat.simplex.app" @@ -51,6 +52,12 @@ public let dbContainerGroupDefault = EnumDefault( public let chatLastStartGroupDefault = DateDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CHAT_LAST_START) +public let ntfPreviewModeGroupDefault = EnumDefault( + defaults: groupDefaults, + forKey: GROUP_DEFAULT_NTF_PREVIEW_MODE, + withDefault: .message +) + public class DateDefault { var defaults: UserDefaults var key: String diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 1b97a14a0c..04bf5a50dd 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -426,6 +426,13 @@ public struct GroupMember: Decodable { } } + public var chatViewName: String { + get { + let p = memberProfile + return p.displayName + (p.fullName == "" || p.fullName == p.displayName ? "" : " / \(p.fullName)") + } + } + public static let sampleData = GroupMember( groupMemberId: 1, memberId: "abcd", diff --git a/apps/ios/SimpleXChat/Notifications.swift b/apps/ios/SimpleXChat/Notifications.swift index 535dc2c5bb..8f27063242 100644 --- a/apps/ios/SimpleXChat/Notifications.swift +++ b/apps/ios/SimpleXChat/Notifications.swift @@ -14,37 +14,59 @@ public let ntfCategoryContactRequest = "NTF_CAT_CONTACT_REQUEST" public let ntfCategoryContactConnected = "NTF_CAT_CONTACT_CONNECTED" public let ntfCategoryMessageReceived = "NTF_CAT_MESSAGE_RECEIVED" public let ntfCategoryCallInvitation = "NTF_CAT_CALL_INVITATION" +public let ntfCategoryConnectionEvent = "NTF_CAT_CONNECTION_EVENT" public let ntfCategoryCheckMessage = "NTF_CAT_CHECK_MESSAGE" -// TODO remove -public let ntfCategoryCheckingMessages = "NTF_CAT_CHECKING_MESSAGES" public let appNotificationId = "chat.simplex.app.notification" +let contactHidden = NSLocalizedString("Contact hidden:", comment: "notification") + public func createContactRequestNtf(_ contactRequest: UserContactRequest) -> UNMutableNotificationContent { - createNotification( + let hideContent = ntfPreviewModeGroupDefault.get() == .hidden + return createNotification( categoryIdentifier: ntfCategoryContactRequest, - title: String.localizedStringWithFormat(NSLocalizedString("%@ wants to connect!", comment: "notification title"), contactRequest.displayName), - body: String.localizedStringWithFormat(NSLocalizedString("Accept contact request from %@?", comment: "notification body"), contactRequest.chatViewName), + title: String.localizedStringWithFormat( + NSLocalizedString("%@ wants to connect!", comment: "notification title"), + hideContent ? NSLocalizedString("Somebody", comment: "notification title") : contactRequest.displayName + ), + body: String.localizedStringWithFormat( + NSLocalizedString("Accept contact request from %@?", comment: "notification body"), + hideContent ? NSLocalizedString("this contact", comment: "notification title") : contactRequest.chatViewName + ), targetContentIdentifier: nil, userInfo: ["chatId": contactRequest.id, "contactRequestId": contactRequest.apiId] ) } public func createContactConnectedNtf(_ contact: Contact) -> UNMutableNotificationContent { - createNotification( + let hideContent = ntfPreviewModeGroupDefault.get() == .hidden + return createNotification( categoryIdentifier: ntfCategoryContactConnected, - title: String.localizedStringWithFormat(NSLocalizedString("%@ is connected!", comment: "notification title"), contact.displayName), - body: String.localizedStringWithFormat(NSLocalizedString("You can now send messages to %@", comment: "notification body"), contact.chatViewName), + title: String.localizedStringWithFormat( + NSLocalizedString("%@ is connected!", comment: "notification title"), + hideContent ? NSLocalizedString("A new contact", comment: "notification title") : contact.displayName + ), + body: String.localizedStringWithFormat( + NSLocalizedString("You can now send messages to %@", comment: "notification body"), + hideContent ? NSLocalizedString("this contact", comment: "notification title") : contact.chatViewName + ), targetContentIdentifier: contact.id // userInfo: ["chatId": contact.id, "contactId": contact.apiId] ) } public func createMessageReceivedNtf(_ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutableNotificationContent { - createNotification( + let previewMode = ntfPreviewModeGroupDefault.get() + var title: String + if case let .group(groupInfo) = cInfo, case let .groupRcv(groupMember) = cItem.chatDir { + title = groupMsgNtfTitle(groupInfo, groupMember, hideContent: previewMode == .hidden) + } else { + title = previewMode == .hidden ? contactHidden : "\(cInfo.chatViewName):" + } + return createNotification( categoryIdentifier: ntfCategoryMessageReceived, - title: "\(cInfo.chatViewName):", - body: hideSecrets(cItem), + title: title, + body: previewMode == .message ? hideSecrets(cItem) : NSLocalizedString("new message", comment: "notification"), targetContentIdentifier: cInfo.id // userInfo: ["chatId": cInfo.id, "chatItemId": cItem.id] ) @@ -54,15 +76,55 @@ public func createCallInvitationNtf(_ invitation: RcvCallInvitation) -> UNMutabl let text = invitation.callType.media == .video ? NSLocalizedString("Incoming video call", comment: "notification") : NSLocalizedString("Incoming audio call", comment: "notification") + let hideContent = ntfPreviewModeGroupDefault.get() == .hidden return createNotification( categoryIdentifier: ntfCategoryCallInvitation, - title: "\(invitation.contact.chatViewName):", + title: hideContent ? contactHidden : "\(invitation.contact.chatViewName):", body: text, targetContentIdentifier: nil, userInfo: ["chatId": invitation.contact.id] ) } +public func createConnectionEventNtf(_ connEntity: ConnectionEntity) -> UNMutableNotificationContent { + let hideContent = ntfPreviewModeGroupDefault.get() == .hidden + var title: String + var body: String? = nil + var targetContentIdentifier: String? = nil + switch connEntity { + case let .rcvDirectMsgConnection(_, contact): + if let contact = contact { + title = hideContent ? contactHidden : "\(contact.chatViewName):" + targetContentIdentifier = contact.id + } else { + title = NSLocalizedString("New contact:", comment: "notification") + } + body = NSLocalizedString("message received", comment: "notification") + case let .rcvGroupMsgConnection(_, groupInfo, groupMember): + title = groupMsgNtfTitle(groupInfo, groupMember, hideContent: hideContent) + body = NSLocalizedString("message received", comment: "notification") + targetContentIdentifier = groupInfo.id + case .sndFileConnection: + title = NSLocalizedString("Sent file event", comment: "notification") + case .rcvFileConnection: + title = NSLocalizedString("Received file event", comment: "notification") + case .userContactConnection: + title = NSLocalizedString("New contact request", comment: "notification") + } + return createNotification( + categoryIdentifier: ntfCategoryCallInvitation, + title: title, + body: body, + targetContentIdentifier: targetContentIdentifier + ) +} + +private func groupMsgNtfTitle(_ groupInfo: GroupInfo, _ groupMember: GroupMember, hideContent: Bool) -> String { + hideContent + ? NSLocalizedString("Group message:", comment: "notification") + : "#\(groupInfo.displayName) \(groupMember.chatViewName):" +} + public func createNotification(categoryIdentifier: String, title: String, subtitle: String? = nil, body: String? = nil, targetContentIdentifier: String? = nil, userInfo: [AnyHashable : Any] = [:]) -> UNMutableNotificationContent { let content = UNMutableNotificationContent()