Save notification when it comes in from Firebase, updates UI

This commit is contained in:
Philipp Heckel 2022-05-17 20:55:55 -04:00
parent cdbeeee531
commit 1660289c4c
11 changed files with 254 additions and 202 deletions

View file

@ -29,6 +29,9 @@
9474F20C283321C300CDE4DD /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F20B283321C300CDE4DD /* Notification.swift */; };
9474F20F283326C500CDE4DD /* ApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F20E283326C500CDE4DD /* ApiService.swift */; };
9474F212283327C200CDE4DD /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F211283327C200CDE4DD /* Helpers.swift */; };
9474F2132834755A00CDE4DD /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F20B283321C300CDE4DD /* Notification.swift */; };
9474F2142834755E00CDE4DD /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F1FE28316ACE00CDE4DD /* Subscription.swift */; };
9474F2152834758700CDE4DD /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F211283327C200CDE4DD /* Helpers.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -331,9 +334,12 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9474F2132834755A00CDE4DD /* Notification.swift in Sources */,
9474F2152834758700CDE4DD /* Helpers.swift in Sources */,
9474F1E7282F3FFD00CDE4DD /* NotificationService.swift in Sources */,
9474F2052831D51500CDE4DD /* Store.swift in Sources */,
9474F2062831D73C00CDE4DD /* Model.xcdatamodeld in Sources */,
9474F2142834755E00CDE4DD /* Subscription.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View file

@ -3,176 +3,179 @@ import SafariServices
import UserNotifications
import Firebase
import FirebaseCore
import CoreData
// https://stackoverflow.com/a/41783666/1440785
// https://stackoverflow.com/questions/47374903/viewing-core-data-data-from-your-app-on-a-device
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let gcmMessageIDKey = "gcm.message_id"
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
print("ApplicationDelegate didFinishLaunchingWithOptions.")
// FirebaseApp.configure() DOES NOT WORK
FirebaseConfiguration.shared.setLoggerLevel(.max)
Messaging.messaging().delegate = self
registerForPushNotifications()
UNUserNotificationCenter.current().delegate = self
var window: UIWindow?
let store = Store.shared
print("Documents Directory: ", FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last ?? "Not Found!")
// Check if launched from notification
let notificationOption = launchOptions?[.remoteNotification]
// 1
if
let notification = notificationOption as? [String: AnyObject],
let aps = notification["aps"] as? [String: AnyObject] {
print("there is a new item")
// 2
// 3
(window?.rootViewController as? UITabBarController)?.selectedIndex = 1
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
print("ApplicationDelegate didFinishLaunchingWithOptions.")
// FirebaseApp.configure() DOES NOT WORK
FirebaseConfiguration.shared.setLoggerLevel(.max)
Messaging.messaging().delegate = self
registerForPushNotifications()
UNUserNotificationCenter.current().delegate = self
print("Documents Directory: ", FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last ?? "Not Found!")
// Check if launched from notification
let notificationOption = launchOptions?[.remoteNotification]
// 1
if
let notification = notificationOption as? [String: AnyObject],
let aps = notification["aps"] as? [String: AnyObject] {
print("there is a new item")
// 2
// 3
(window?.rootViewController as? UITabBarController)?.selectedIndex = 1
}
return true
}
return true
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
guard let aps = userInfo["aps"] as? [String: AnyObject] else {
completionHandler(.failed)
return
}
print("didReceiveRemoteNotification")
print(userInfo)
}
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any]) {
print("didReceiveRemoteNotification 2")
guard let aps = userInfo["aps"] as? [String: AnyObject] else {
return
}
print(userInfo)
}
func application(
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
print("Failed to register: \(error)")
}
// This function is added here only for debugging purposes, and can be removed if swizzling is enabled.
// If swizzling is disabled then this function must be implemented so that the APNs token can be paired to
// the FCM registration token.
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
print("APNs token retrieved: \(deviceToken)")
// With swizzling disabled you must set the APNs token here.
Messaging.messaging().apnsToken = deviceToken
let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
let token = tokenParts.joined()
print("Device Token: \(token)")
}
func registerForPushNotifications() {
UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] granted, _ in
print("granted: \(granted)")
guard granted else { return }
self?.getNotificationSettings()
}
}
func getNotificationSettings() {
UNUserNotificationCenter.current().getNotificationSettings { settings in
print("Notification settings: \(settings)")
guard settings.authorizationStatus == .authorized else { return }
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
guard let aps = userInfo["aps"] as? [String: AnyObject] else {
completionHandler(.failed)
return
}
print("didReceiveRemoteNotification")
print(userInfo)
}
}
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any]) {
print("didReceiveRemoteNotification 2")
guard let aps = userInfo["aps"] as? [String: AnyObject] else {
return
}
print(userInfo)
}
func application(
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
print("Failed to register: \(error)")
}
// This function is added here only for debugging purposes, and can be removed if swizzling is enabled.
// If swizzling is disabled then this function must be implemented so that the APNs token can be paired to
// the FCM registration token.
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
print("APNs token retrieved: \(deviceToken)")
// With swizzling disabled you must set the APNs token here.
Messaging.messaging().apnsToken = deviceToken
let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
let token = tokenParts.joined()
print("Device Token: \(token)")
}
func registerForPushNotifications() {
UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] granted, _ in
print("granted: \(granted)")
guard granted else { return }
self?.getNotificationSettings()
}
}
func getNotificationSettings() {
UNUserNotificationCenter.current().getNotificationSettings { settings in
print("Notification settings: \(settings)")
guard settings.authorizationStatus == .authorized else { return }
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
}
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "Model")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// MARK: - Core Data Saving support
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
// [START ios_10_message_handling]
//@available(iOS 10, *)
extension AppDelegate: UNUserNotificationCenterDelegate {
// Receive displayed notifications for iOS 10 devices.
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions)
-> Void) {
print("willPresent")
let userInfo = notification.request.content.userInfo
// With swizzling disabled you must let Messaging know about the message, for Analytics
// Messaging.messaging().appDidReceiveMessage(userInfo)
// [START_EXCLUDE]
// Print message ID.
if let messageID = userInfo[gcmMessageIDKey] {
print("Message ID: \(messageID)")
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
let userInfo = notification.request.content.userInfo
print("willPresent", userInfo)
store.saveNotification(fromUserInfo: userInfo)
completionHandler([[.alert, .sound]])
}
// [END_EXCLUDE]
// Print full message.
print(userInfo)
// Change this to your preferred presentation option
completionHandler([[.alert, .sound]])
}
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
print("didReceive")
let userInfo = response.notification.request.content.userInfo
// [START_EXCLUDE]
// Print message ID.
if let messageID = userInfo[gcmMessageIDKey] {
print("Message ID: \(messageID)")
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
print("didReceive")
let userInfo = response.notification.request.content.userInfo
completionHandler()
}
// [END_EXCLUDE]
// With swizzling disabled you must let Messaging know about the message, for Analytics
// Messaging.messaging().appDidReceiveMessage(userInfo)
// Print full message.
print(userInfo)
completionHandler()
}
}
// [END ios_10_message_handling]
extension AppDelegate: MessagingDelegate {
// [START refresh_token]
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
print("Firebase registration token: \(String(describing: fcmToken))")
let dataDict: [String: String] = ["token": fcmToken ?? ""]
NotificationCenter.default.post(
name: UserNotifications.Notification.Name("FCMToken"),
object: nil,
userInfo: dataDict
)
// TODO: If necessary send token to application server.
// Note: This callback is fired at each app startup and whenever a new token is generated.
}
// [END refresh_token]
// [START refresh_token]
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
print("Firebase registration token: \(String(describing: fcmToken))")
let dataDict: [String: String] = ["token": fcmToken ?? ""]
NotificationCenter.default.post(
name: UserNotifications.Notification.Name("FCMToken"),
object: nil,
userInfo: dataDict
)
// TODO: If necessary send token to application server.
// Note: This callback is fired at each app startup and whenever a new token is generated.
}
}

View file

@ -11,7 +11,7 @@ import Firebase
@main
struct AppMain: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate: AppDelegate
@StateObject private var store = Store()
@StateObject private var store = Store.shared
init() {
FirebaseApp.configure()

View file

@ -30,3 +30,10 @@ extension Notification {
return dateFormatter.string(from: date)
}
}
struct Message: Decodable {
var id: String
var time: Int64
var message: String?
var title: String?
}

View file

@ -9,13 +9,79 @@ import Foundation
import CoreData
class Store: ObservableObject {
let container = NSPersistentContainer(name: "Model")
static let shared = Store()
let container: NSPersistentContainer
var context: NSManagedObjectContext
init() {
container = NSPersistentContainer(name: "Model")
container.loadPersistentStores { description, error in
if let error = error {
print("Core Data failed to load: \(error.localizedDescription)")
}
}
context = container.viewContext
}
func saveSubscription(baseUrl: String, topic: String) {
let subscription = Subscription(context: context)
subscription.baseUrl = appBaseUrl
subscription.topic = topic
try? context.save()
}
func getSubscription(baseUrl: String, topic: String) -> Subscription? {
let fetchRequest = Subscription.fetchRequest()
let baseUrlPredicate = NSPredicate(format: "baseUrl = %@", baseUrl)
let topicPredicate = NSPredicate(format: "topic = %@", topic)
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [baseUrlPredicate, topicPredicate])
return try? context.fetch(fetchRequest).first
}
func saveNotification(fromUserInfo userInfo: [AnyHashable: Any]) {
guard let id = userInfo["id"] as? String,
let topic = userInfo["topic"] as? String,
let time = userInfo["time"] as? String,
let timeInt = Int64(time),
let message = userInfo["message"] as? String else {
print("Unknown or irrelevant message", userInfo)
return
}
guard let subscription = getSubscription(baseUrl: appBaseUrl, topic: topic) else {
print("Subscription for topic \(topic) unknown")
return
}
do {
let notification = Notification(context: context)
notification.id = id
notification.time = timeInt
notification.message = message
notification.title = userInfo["title"] as? String ?? ""
subscription.addToNotifications(notification)
try context.save()
} catch let error {
print(error)
context.rollback()
}
}
func saveNotification(fromMessage message: Message, subscription: Subscription) {
do {
let notification = Notification(context: context)
notification.id = message.id
notification.time = message.time
notification.message = message.message ?? ""
notification.title = message.title ?? ""
subscription.addToNotifications(notification)
try context.save()
} catch let error {
print(error)
context.rollback()
}
}
}

View file

@ -59,9 +59,3 @@ class ApiService: NSObject {
}
}
struct Message: Decodable {
var id: String
var time: Int64
var message: String?
var title: String?
}

View file

@ -7,6 +7,8 @@
import Foundation
let appBaseUrl = "http://192.168.1.4" // FIXME
func topicUrl(baseUrl: String, topic: String) -> String {
return "\(baseUrl)/\(topic)"
}

View file

@ -16,17 +16,20 @@ struct NotificationListView: View {
@Environment(\.presentationMode) var presentationMode
@ObservedObject var subscription: Subscription
var notifications: [Notification]
@State private var editMode = EditMode.inactive
@State private var selection = Set<Notification>()
@State private var showAlert = false
@State private var activeAlert: ActiveAlert = .clear
private let store = Store.shared
var body: some View {
NavigationView {
List(selection: $selection) {
ForEach(Array(subscription.notifications! as Set).reversed(), id: \.self) { notification in
ForEach(notifications, id: \.self) { notification in
NotificationRowView(notification: notification as! Notification)
}
}
@ -138,18 +141,7 @@ struct NotificationListView: View {
print("Saving new messages to subscription \(subscription.urlString())", messages)
DispatchQueue.main.async {
for message in messages {
do {
let notification = Notification(context: context)
notification.id = message.id
notification.time = message.time
notification.message = message.message ?? ""
notification.title = message.title ?? ""
subscription.addToNotifications(notification)
try context.save()
} catch let error {
print(error)
context.rollback()
}
store.saveNotification(fromMessage: message, subscription: subscription)
}
}
}
@ -166,7 +158,7 @@ struct NotificationListView: View {
self.editMode = .active
self.selection = Set<Notification>()
}) {
Text("Select Messages")
Text("Select messages")
}
} else {
return Button(action: {

View file

@ -12,6 +12,7 @@ struct SubscriptionAddView: View {
@Environment(\.managedObjectContext) var context
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@State private var topic: String = ""
private let store = Store.shared
var body: some View {
VStack {
@ -45,14 +46,10 @@ struct SubscriptionAddView: View {
}
private func subscribeAction() {
print("Subscribing to topic \(topic)")
print("Subscribing to \(topicUrl(baseUrl: appBaseUrl, topic: topic))")
Messaging.messaging().subscribe(toTopic: topic)
let subscription = Subscription(context: context)
subscription.baseUrl = "https://ntfy.sh"
subscription.topic = topic
try? context.save()
store.saveSubscription(baseUrl: appBaseUrl, topic: topic)
presentationMode.wrappedValue.dismiss()
}
}

View file

@ -19,8 +19,9 @@ struct SubscriptionsList: View {
NavigationView {
List {
ForEach(subscriptions) { subscription in
let notifications = subscription.notifications!.sortedArray(using: [NSSortDescriptor(key: "time", ascending: false)]) as [Notification]
ZStack {
NavigationLink(destination: NotificationListView(subscription: subscription)) {
NavigationLink(destination: NotificationListView(subscription: subscription, notifications: notifications)) {
EmptyView()
}
.opacity(0.0)

View file

@ -13,33 +13,17 @@ import CoreData
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
var store: Store?
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
store = Store()
let context = store?.container.viewContext
print("NotificationService didReceive")
if let bestAttemptContent = bestAttemptContent {
// Modify the notification content here...
bestAttemptContent.title = "\(bestAttemptContent.title) [modified]"
let userInfo = bestAttemptContent.userInfo
dump(userInfo)
if let notificationId = userInfo["id"] as? String,
let notificationTimestamp = userInfo["time"] as? String,
let notificationTimestampInt = Int64(notificationTimestamp),
let notificationMessage = userInfo["message"] as? String {
print("notification service \(notificationId)")
/*let notification = Notification()
notification.id = notificationId
notification.time = notificationTimestampInt
notification.message = notificationMessage*/
// try? context?.save()
}
Store.shared.saveNotification(fromUserInfo: userInfo)
contentHandler(bestAttemptContent)
}
}
@ -51,5 +35,5 @@ class NotificationService: UNNotificationServiceExtension {
contentHandler(bestAttemptContent)
}
}
}