From 372e9464193a9a163ca994896fe8754fd62464ec Mon Sep 17 00:00:00 2001 From: Tom Caputi Date: Sat, 28 Oct 2023 03:07:56 -0400 Subject: [PATCH 01/12] receiving notifications now updates ui properly --- ntfy.xcodeproj/project.pbxproj | 22 +-- .../xcshareddata/swiftpm/Package.resolved | 12 +- .../xcshareddata/xcschemes/ntfy.xcscheme | 77 ++++++++++ .../xcshareddata/xcschemes/ntfyNSE.xcscheme | 100 +++++++++++++ ntfy/App/AppDelegate.swift | 23 ++- ntfy/App/AppMain.swift | 5 - .../AppIcon.appiconset/Contents.json | 134 ++++++++++-------- ntfy/Info.plist | 2 + ntfy/Persistence/Store.swift | 12 +- ntfy/Views/NotificationListView.swift | 12 +- ntfy/Views/SubscriptionListView.swift | 13 ++ ntfy/ntfy.entitlements | 2 +- ntfyNSE/ntfyNSE.entitlements | 2 +- 13 files changed, 319 insertions(+), 97 deletions(-) create mode 100644 ntfy.xcodeproj/xcshareddata/xcschemes/ntfy.xcscheme create mode 100644 ntfy.xcodeproj/xcshareddata/xcschemes/ntfyNSE.xcscheme diff --git a/ntfy.xcodeproj/project.pbxproj b/ntfy.xcodeproj/project.pbxproj index 5526708..8aff12f 100644 --- a/ntfy.xcodeproj/project.pbxproj +++ b/ntfy.xcodeproj/project.pbxproj @@ -538,17 +538,19 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + APP_BASE_URL = "http://192.168.1.7"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = ntfy/ntfy.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_ASSET_PATHS = "\"ntfy/Assets/Preview Content\""; - DEVELOPMENT_TEAM = YXQ4AMS4B4; + DEVELOPMENT_TEAM = MZWHX5Z44T; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ntfy/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ntfy; + INFOPLIST_KEY_NSCameraUsageDescription = "We need access to the camera to scan QR codes."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; @@ -560,7 +562,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.2; - PRODUCT_BUNDLE_IDENTIFIER = io.heckel.ntfy; + PRODUCT_BUNDLE_IDENTIFIER = com.tcaputi.ntfy; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -572,17 +574,19 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + APP_BASE_URL = "http://192.168.1.7"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = ntfy/ntfy.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_ASSET_PATHS = "\"ntfy/Assets/Preview Content\""; - DEVELOPMENT_TEAM = YXQ4AMS4B4; + DEVELOPMENT_TEAM = MZWHX5Z44T; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ntfy/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ntfy; + INFOPLIST_KEY_NSCameraUsageDescription = "We need access to the camera to scan QR codes."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; @@ -594,7 +598,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.2; - PRODUCT_BUNDLE_IDENTIFIER = io.heckel.ntfy; + PRODUCT_BUNDLE_IDENTIFIER = com.tcaputi.ntfy; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -605,10 +609,11 @@ 9474F1ED282F3FFD00CDE4DD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + APP_BASE_URL = "http://192.168.1.7"; CODE_SIGN_ENTITLEMENTS = ntfyNSE/ntfyNSE.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 3; - DEVELOPMENT_TEAM = YXQ4AMS4B4; + DEVELOPMENT_TEAM = MZWHX5Z44T; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ntfyNSE/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ntfyNSE; @@ -620,7 +625,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.2; - PRODUCT_BUNDLE_IDENTIFIER = io.heckel.ntfy.ntfyNSE; + PRODUCT_BUNDLE_IDENTIFIER = com.tcaputi.ntfy.ntfyNSE; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; @@ -632,10 +637,11 @@ 9474F1EE282F3FFD00CDE4DD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + APP_BASE_URL = "http://192.168.1.7"; CODE_SIGN_ENTITLEMENTS = ntfyNSE/ntfyNSE.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 3; - DEVELOPMENT_TEAM = YXQ4AMS4B4; + DEVELOPMENT_TEAM = MZWHX5Z44T; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ntfyNSE/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ntfyNSE; @@ -647,7 +653,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.2; - PRODUCT_BUNDLE_IDENTIFIER = io.heckel.ntfy.ntfyNSE; + PRODUCT_BUNDLE_IDENTIFIER = com.tcaputi.ntfy.ntfyNSE; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/ntfy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ntfy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a445f0c..e1ec2d0 100644 --- a/ntfy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ntfy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk", "state" : { - "revision" : "cfa854c9c1073c4d1b83b20dfcb1ef7ceb85388b", - "version" : "9.0.0" + "revision" : "7e80c25b51c2ffa238879b07fbfc5baa54bb3050", + "version" : "9.6.0" } }, { @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleAppMeasurement.git", "state" : { - "revision" : "6a3123fab90f3884167990bee9bb30097d99c98c", - "version" : "9.0.0" + "revision" : "c1cfde8067668027b23a42c29d11c246152fe046", + "version" : "9.6.0" } }, { @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/nanopb.git", "state" : { - "revision" : "7ee9ef9f627d85cbe1b8c4f49a3ed26eed216c77", - "version" : "2.30908.0" + "revision" : "819d0a2173aff699fb8c364b6fb906f7cdb1a692", + "version" : "2.30909.0" } }, { diff --git a/ntfy.xcodeproj/xcshareddata/xcschemes/ntfy.xcscheme b/ntfy.xcodeproj/xcshareddata/xcschemes/ntfy.xcscheme new file mode 100644 index 0000000..5967b20 --- /dev/null +++ b/ntfy.xcodeproj/xcshareddata/xcschemes/ntfy.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ntfy.xcodeproj/xcshareddata/xcschemes/ntfyNSE.xcscheme b/ntfy.xcodeproj/xcshareddata/xcschemes/ntfyNSE.xcscheme new file mode 100644 index 0000000..7a4f970 --- /dev/null +++ b/ntfy.xcodeproj/xcshareddata/xcschemes/ntfyNSE.xcscheme @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ntfy/App/AppDelegate.swift b/ntfy/App/AppDelegate.swift index c0265a5..dcb5551 100644 --- a/ntfy/App/AppDelegate.swift +++ b/ntfy/App/AppDelegate.swift @@ -13,11 +13,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate, ObservableObject { // Implements navigation from notifications, see https://stackoverflow.com/a/70731861/1440785 @Published var selectedBaseUrl: String? = nil - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { Log.d(tag, "Launching AppDelegate") - + + FirebaseApp.configure() + FirebaseConfiguration.shared.setLoggerLevel(.max) + // Register app permissions for push notifications UNUserNotificationCenter.current().delegate = self + Messaging.messaging().delegate = self + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in guard success else { Log.e(self.tag, "Failed to register for local push notifications", error) @@ -28,13 +33,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, ObservableObject { // Register too receive remote notifications application.registerForRemoteNotifications() - - // Set self as messaging delegate - Messaging.messaging().delegate = self - - // Register to "~poll" topic - Messaging.messaging().subscribe(toTopic: pollTopic) - + return true } @@ -137,8 +136,8 @@ extension AppDelegate: UNUserNotificationCenterDelegate { extension AppDelegate: MessagingDelegate { func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { Log.d(tag, "Firebase token received: \(String(describing: fcmToken))") - - // We don't actually need the FCM token, since we're just using topics. - // We still print it so we can see if things were successful. + + // We wait until we have a registration token before subscribing to our pollTopic + Messaging.messaging().subscribe(toTopic: pollTopic) } } diff --git a/ntfy/App/AppMain.swift b/ntfy/App/AppMain.swift index b753e28..1fedf39 100644 --- a/ntfy/App/AppMain.swift +++ b/ntfy/App/AppMain.swift @@ -13,11 +13,6 @@ struct AppMain: App { init() { Log.d(tag, "Launching ntfy 🥳. Welcome!") Log.d(tag, "Base URL is \(Config.appBaseUrl), user agent is \(ApiService.userAgent)") - - // We must configure Firebase here, and not in the AppDelegate. For some reason - // configuring it there did not work. - FirebaseApp.configure() - FirebaseConfiguration.shared.setLoggerLevel(.max) } var body: some Scene { diff --git a/ntfy/Assets/Assets.xcassets/AppIcon.appiconset/Contents.json b/ntfy/Assets/Assets.xcassets/AppIcon.appiconset/Contents.json index 04de9d4..f78687a 100644 --- a/ntfy/Assets/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ntfy/Assets/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -150,6 +150,66 @@ "scale" : "1x", "size" : "1024x1024" }, + { + "filename" : "16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "32.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "32.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "64.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "256.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "512.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "512.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "1024.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + }, { "filename" : "48.png", "idiom" : "watch", @@ -225,6 +285,13 @@ "size" : "51x51", "subtype" : "45mm" }, + { + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "54x54", + "subtype" : "49mm" + }, { "filename" : "172.png", "idiom" : "watch", @@ -256,71 +323,18 @@ "size" : "117x117", "subtype" : "45mm" }, + { + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "129x129", + "subtype" : "49mm" + }, { "filename" : "1024.png", "idiom" : "watch-marketing", "scale" : "1x", "size" : "1024x1024" - }, - { - "filename" : "16.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "16x16" - }, - { - "filename" : "32.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" - }, - { - "filename" : "32.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" - }, - { - "filename" : "64.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "32x32" - }, - { - "filename" : "128.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "128x128" - }, - { - "filename" : "256.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "128x128" - }, - { - "filename" : "256.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" - }, - { - "filename" : "512.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "256x256" - }, - { - "filename" : "512.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "512x512" - }, - { - "filename" : "1024.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "512x512" } ], "info" : { diff --git a/ntfy/Info.plist b/ntfy/Info.plist index f1f6093..6abed55 100644 --- a/ntfy/Info.plist +++ b/ntfy/Info.plist @@ -18,5 +18,7 @@ remote-notification + FirebaseAppDelegateProxyEnabled + diff --git a/ntfy/Persistence/Store.swift b/ntfy/Persistence/Store.swift index 03eab1b..7b40d33 100644 --- a/ntfy/Persistence/Store.swift +++ b/ntfy/Persistence/Store.swift @@ -7,7 +7,7 @@ import Combine class Store: ObservableObject { static let shared = Store() static let tag = "Store" - static let appGroup = "group.io.heckel.ntfy" // Must match app group of ntfy = ntfyNSE targets + static let appGroup = "group.com.tcaputi.ntfy" // Must match app group of ntfy = ntfyNSE targets static let modelName = "ntfy" // Must match .xdatamodeld folder static let prefKeyDefaultBaseUrl = "defaultBaseUrl" @@ -43,9 +43,13 @@ class Store: ObservableObject { NotificationCenter.default .publisher(for: .NSPersistentStoreRemoteChange) .sink { value in - Log.d(Store.tag, "Remote change detected, refreshing view", value) + // TODO: this could probably broadcast the name of the channel + // so that only relevant views can update. + Log.d(Store.tag, "Remote change detected, refreshing views", value) + DispatchQueue.main.async { self.hardRefresh() + NotificationCenter.default.post(name: .notificationReceived, object: nil) } } .store(in: &cancellables) @@ -266,3 +270,7 @@ extension Store { return notification } } + +extension Foundation.Notification.Name { + static let notificationReceived = Foundation.Notification.Name("notificationReceived") +} diff --git a/ntfy/Views/NotificationListView.swift b/ntfy/Views/NotificationListView.swift index 48b3149..4f3815b 100644 --- a/ntfy/Views/NotificationListView.swift +++ b/ntfy/Views/NotificationListView.swift @@ -22,15 +22,23 @@ struct NotificationListView: View { private var subscriptionManager: SubscriptionManager { return SubscriptionManager(store: store) } - + var body: some View { + let notificationReceived = Foundation.Notification.Name("notificationReceived") + if #available(iOS 15.0, *) { notificationList .refreshable { subscriptionManager.poll(subscription) + }.onReceive(NotificationCenter.default.publisher(for: notificationReceived)) { _ in + // Handle the notification + subscriptionManager.poll(subscription) } } else { - notificationList + notificationList.onReceive(NotificationCenter.default.publisher(for: notificationReceived)) { _ in + // Handle the notification + subscriptionManager.poll(subscription) + } } } diff --git a/ntfy/Views/SubscriptionListView.swift b/ntfy/Views/SubscriptionListView.swift index 537b727..5bcd4bc 100644 --- a/ntfy/Views/SubscriptionListView.swift +++ b/ntfy/Views/SubscriptionListView.swift @@ -15,6 +15,8 @@ struct SubscriptionListView: View { } var body: some View { + let notificationReceived = Foundation.Notification.Name("notificationReceived") + NavigationView { if #available(iOS 15.0, *) { subscriptionList @@ -22,9 +24,20 @@ struct SubscriptionListView: View { subscriptions.forEach { subscription in subscriptionManager.poll(subscription) } + }.onReceive(NotificationCenter.default.publisher(for: notificationReceived)) { _ in + // Handle the notification + subscriptions.forEach { subscription in + subscriptionManager.poll(subscription) + } } } else { subscriptionList + .onReceive(NotificationCenter.default.publisher(for: notificationReceived)) { _ in + // Handle the notification + subscriptions.forEach { subscription in + subscriptionManager.poll(subscription) + } + } .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button { diff --git a/ntfy/ntfy.entitlements b/ntfy/ntfy.entitlements index 384fbad..d16d692 100644 --- a/ntfy/ntfy.entitlements +++ b/ntfy/ntfy.entitlements @@ -6,7 +6,7 @@ development com.apple.security.application-groups - group.io.heckel.ntfy + group.com.tcaputi.ntfy diff --git a/ntfyNSE/ntfyNSE.entitlements b/ntfyNSE/ntfyNSE.entitlements index 384fbad..d16d692 100644 --- a/ntfyNSE/ntfyNSE.entitlements +++ b/ntfyNSE/ntfyNSE.entitlements @@ -6,7 +6,7 @@ development com.apple.security.application-groups - group.io.heckel.ntfy + group.com.tcaputi.ntfy From 740e1a7091cac454bc53d3079bce4a7d27843ccc Mon Sep 17 00:00:00 2001 From: Tom Caputi Date: Sat, 28 Oct 2023 04:23:23 -0400 Subject: [PATCH 02/12] things kinda work. adding debounce --- ntfy.xcodeproj/project.pbxproj | 4 ++ ntfy/Info.plist | 6 ++- ntfy/Utils/QRScannerUIView.swift | 70 ++++++++++++++++++++++++ ntfy/Views/SubscriptionAddView.swift | 81 ++++++++++++++++++++++++---- 4 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 ntfy/Utils/QRScannerUIView.swift diff --git a/ntfy.xcodeproj/project.pbxproj b/ntfy.xcodeproj/project.pbxproj index 8aff12f..daad103 100644 --- a/ntfy.xcodeproj/project.pbxproj +++ b/ntfy.xcodeproj/project.pbxproj @@ -50,6 +50,7 @@ 94CD196A283E666900973B93 /* EmojiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD1969283E666900973B93 /* EmojiManager.swift */; }; 94CD196B283E666900973B93 /* EmojiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD1969283E666900973B93 /* EmojiManager.swift */; }; 94E9196C28353E0100F30170 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F216283531A200CDE4DD /* Log.swift */; }; + E278CB332AECECCA004B9143 /* QRScannerUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E278CB322AECECCA004B9143 /* QRScannerUIView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -111,6 +112,7 @@ 94B736D6284AF9BE003D69FB /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; 94CD1965283E662900973B93 /* emojis.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = emojis.json; sourceTree = ""; }; 94CD1969283E666900973B93 /* EmojiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiManager.swift; sourceTree = ""; }; + E278CB322AECECCA004B9143 /* QRScannerUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScannerUIView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -237,6 +239,7 @@ 94867142283EC9950093C7A4 /* Actions.swift */, 948671462841B0B20093C7A4 /* NotificationContent.swift */, 948671492841D0CE0093C7A4 /* ActionExecutor.swift */, + E278CB322AECECCA004B9143 /* QRScannerUIView.swift */, ); path = Utils; sourceTree = ""; @@ -375,6 +378,7 @@ 9474F1F22830825600CDE4DD /* SubscriptionListView.swift in Sources */, 9486714A2841D0CE0093C7A4 /* ActionExecutor.swift in Sources */, 9474F1FD2831311A00CDE4DD /* SubscriptionAddView.swift in Sources */, + E278CB332AECECCA004B9143 /* QRScannerUIView.swift in Sources */, 9474F1FF28316ACE00CDE4DD /* Subscription.swift in Sources */, 94CD196A283E666900973B93 /* EmojiManager.swift in Sources */, 9474F1C1282F2AA700CDE4DD /* AppMain.swift in Sources */, diff --git a/ntfy/Info.plist b/ntfy/Info.plist index 6abed55..0ad242b 100644 --- a/ntfy/Info.plist +++ b/ntfy/Info.plist @@ -18,7 +18,9 @@ remote-notification - FirebaseAppDelegateProxyEnabled - + FirebaseAppDelegateProxyEnabled + + NSCameraUsageDescription + We need access to the camera for QR code scanning. diff --git a/ntfy/Utils/QRScannerUIView.swift b/ntfy/Utils/QRScannerUIView.swift new file mode 100644 index 0000000..5b607d9 --- /dev/null +++ b/ntfy/Utils/QRScannerUIView.swift @@ -0,0 +1,70 @@ +import AVFoundation +import SwiftUI + +struct QRScannerUIView: UIViewRepresentable { + var onCodeDetected: (String) -> Void + + func makeUIView(context: Context) -> some UIView { + let view = QRScannerUIViewContainer() + + let captureSession = AVCaptureSession() + + guard let videoCaptureDevice = AVCaptureDevice.default(for: .video), + let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice), + captureSession.canAddInput(videoInput) else { + return view + } + captureSession.addInput(videoInput) + + let metadataOutput = AVCaptureMetadataOutput() + captureSession.addOutput(metadataOutput) + + metadataOutput.setMetadataObjectsDelegate(context.coordinator, queue: DispatchQueue.main) + metadataOutput.metadataObjectTypes = [.qr] + + let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) + previewLayer.frame = view.layer.bounds + previewLayer.videoGravity = .resizeAspectFill + view.layer.insertSublayer(previewLayer, at: 0) + + // Move the startRunning call to a background thread + DispatchQueue.global(qos: .userInitiated).async { + captureSession.startRunning() + } + + view.previewLayer = previewLayer + view.captureSession = captureSession + return view + } + + func updateUIView(_ uiView: UIViewType, context: Context) {} + + func makeCoordinator() -> Coordinator { + Coordinator(onCodeDetected: onCodeDetected) + } + + class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate { + var onCodeDetected: (String) -> Void + + init(onCodeDetected: @escaping (String) -> Void) { + self.onCodeDetected = onCodeDetected + } + + func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { + if let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject, let stringValue = metadataObject.stringValue { + onCodeDetected(stringValue) + } + } + } +} + + +class QRScannerUIViewContainer: UIView { + var previewLayer: AVCaptureVideoPreviewLayer? + var captureSession: AVCaptureSession? + + override func layoutSubviews() { + super.layoutSubviews() + previewLayer?.frame = self.bounds + } +} diff --git a/ntfy/Views/SubscriptionAddView.swift b/ntfy/Views/SubscriptionAddView.swift index 2fc8645..8b6b4cb 100644 --- a/ntfy/Views/SubscriptionAddView.swift +++ b/ntfy/Views/SubscriptionAddView.swift @@ -1,4 +1,5 @@ import SwiftUI +import AVFoundation struct SubscriptionAddView: View { private let tag = "SubscriptionAddView" @@ -17,6 +18,7 @@ struct SubscriptionAddView: View { @State private var loading = false @State private var addError: String? @State private var loginError: String? + @State private var hasCameraPermission: Bool = false private var subscriptionManager: SubscriptionManager { return SubscriptionManager(store: store) @@ -24,19 +26,29 @@ struct SubscriptionAddView: View { var body: some View { NavigationView { + VStack { + addView + // TODO: hide this if permission not granted + QRScannerUIView { code in + onQRCodeScanned(text: code) + } + .frame(height: 250) // You can adjust the height as needed. + .padding() + .onAppear(perform: checkCameraPermission) + } + // This is a little weird, but it works. The nagivation link for the login view - // is rendered in the backgroun (it's hidden), abd we toggle it manually. + // is rendered in the background (it's hidden), abd we toggle it manually. // If anyone has a better way to do a two-page layout let me know. - addView - .background(Group { - NavigationLink( - destination: loginView, - isActive: $showLogin - ) { - EmptyView() - } - }) + .background(Group { + NavigationLink( + destination: loginView, + isActive: $showLogin + ) { + EmptyView() + } + }) } } @@ -129,6 +141,55 @@ struct SubscriptionAddView: View { return topic.trimmingCharacters(in: .whitespaces) } + private func checkCameraPermission() { + switch AVCaptureDevice.authorizationStatus(for: .video) { + case .authorized: + self.hasCameraPermission = true + + case .notDetermined: + AVCaptureDevice.requestAccess(for: .video) { granted in + DispatchQueue.main.async { + self.hasCameraPermission = granted + if !granted { + self.hasCameraPermission = false + } + } + } + + case .denied, .restricted: + self.hasCameraPermission = false + + @unknown default: + break + } + } + + func onQRCodeScanned(text: String){ + // Check if the text is a valid URL with HTTP or HTTPS scheme + guard let url = URL(string: text), let scheme = url.scheme, ["http", "https"].contains(scheme) else { + return + } + + // Extract the base URL without the path + var components = URLComponents(url: url, resolvingAgainstBaseURL: false) + components?.path = "" + guard let foundBaseUrl = components?.url else { + return + } + + // Extract the route from the original URL + baseUrl = foundBaseUrl.absoluteString + useAnother = baseUrl != store.getDefaultBaseUrl() + + topic = url.path + if (topic.hasPrefix("/")) { + topic.removeFirst() + } + + print("------> \(baseUrl) : \(topic) : \(useAnother)") + subscribeOrShowLoginAction() + } + private func isAddViewValid() -> Bool { if sanitizedTopic.isEmpty { return false From 93a240dad84b9c06589f58e30a1beaac8a6e9fc6 Mon Sep 17 00:00:00 2001 From: Tom Caputi Date: Sat, 28 Oct 2023 04:40:48 -0400 Subject: [PATCH 03/12] minimum viable product now seems to work --- ntfy/Persistence/SubscriptionManager.swift | 7 +++++++ ntfy/Utils/QRScannerUIView.swift | 16 +++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/ntfy/Persistence/SubscriptionManager.swift b/ntfy/Persistence/SubscriptionManager.swift index 123b271..81560eb 100644 --- a/ntfy/Persistence/SubscriptionManager.swift +++ b/ntfy/Persistence/SubscriptionManager.swift @@ -37,6 +37,13 @@ struct SubscriptionManager { } func poll(_ subscription: Subscription, completionHandler: @escaping ([Message]) -> Void) { + // This is a bit of a hack but it prevents us from polling dead subscriptions + if (subscription.baseUrl == nil) { + Log.d(tag, "Attempting to poll dead subscription failed") + completionHandler([]) + return + } + let user = store.getUser(baseUrl: subscription.baseUrl!)?.toBasicUser() Log.d(tag, "Polling from \(subscription.urlString()) with user \(user?.username ?? "anonymous")") ApiService.shared.poll(subscription: subscription, user: user) { messages, error in diff --git a/ntfy/Utils/QRScannerUIView.swift b/ntfy/Utils/QRScannerUIView.swift index 5b607d9..9ecf8d2 100644 --- a/ntfy/Utils/QRScannerUIView.swift +++ b/ntfy/Utils/QRScannerUIView.swift @@ -45,14 +45,28 @@ struct QRScannerUIView: UIViewRepresentable { class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate { var onCodeDetected: (String) -> Void + private var lastScanDate: Date? + private let debounceInterval: TimeInterval = 3.0 init(onCodeDetected: @escaping (String) -> Void) { self.onCodeDetected = onCodeDetected } + + func qrCodeScanned(_ code: String) { + let now = Date() + + // If it's the first scan or the interval since the last scan is more than the debounce interval + if let lastScan = lastScanDate, now.timeIntervalSince(lastScan) < debounceInterval { + return + } + + onCodeDetected(code) + lastScanDate = now + } func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { if let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject, let stringValue = metadataObject.stringValue { - onCodeDetected(stringValue) + qrCodeScanned(stringValue) } } } From f2dd5fb3f1075e16425fc094fcc438a3521876e1 Mon Sep 17 00:00:00 2001 From: Tom Caputi Date: Mon, 30 Oct 2023 16:35:54 -0400 Subject: [PATCH 04/12] send event from the code that actaully received the notification --- ntfy.xcodeproj/project.pbxproj | 4 ++-- ntfy/App/AppDelegate.swift | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ntfy.xcodeproj/project.pbxproj b/ntfy.xcodeproj/project.pbxproj index daad103..65e2bfa 100644 --- a/ntfy.xcodeproj/project.pbxproj +++ b/ntfy.xcodeproj/project.pbxproj @@ -542,7 +542,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - APP_BASE_URL = "http://192.168.1.7"; + APP_BASE_URL = "http://192.168.105.215"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = ntfy/ntfy.entitlements; @@ -578,7 +578,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - APP_BASE_URL = "http://192.168.1.7"; + APP_BASE_URL = "http://192.168.105.215"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = ntfy/ntfy.entitlements; diff --git a/ntfy/App/AppDelegate.swift b/ntfy/App/AppDelegate.swift index dcb5551..697834b 100644 --- a/ntfy/App/AppDelegate.swift +++ b/ntfy/App/AppDelegate.swift @@ -97,6 +97,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { ) { let userInfo = notification.request.content.userInfo Log.d(tag, "Notification received via userNotificationCenter(willPresent)", userInfo) + NotificationCenter.default.post(name: .notificationReceived, object: nil) completionHandler([[.banner, .sound]]) } @@ -129,6 +130,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { UIApplication.shared.open(url, options: [:], completionHandler: nil) } + NotificationCenter.default.post(name: .notificationReceived, object: nil) completionHandler() } } From 9113857bfb9d291cfe8bbf36c4dca4ae3d267bef Mon Sep 17 00:00:00 2001 From: Tom Caputi Date: Tue, 31 Oct 2023 03:48:55 -0400 Subject: [PATCH 05/12] polling now happens when app reawakes --- ntfy.xcodeproj/project.pbxproj | 4 ++-- ntfy/App/AppDelegate.swift | 5 +++-- ntfy/Persistence/Store.swift | 3 +-- ntfy/Views/NotificationListView.swift | 6 +++--- ntfy/Views/SubscriptionListView.swift | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ntfy.xcodeproj/project.pbxproj b/ntfy.xcodeproj/project.pbxproj index 65e2bfa..daad103 100644 --- a/ntfy.xcodeproj/project.pbxproj +++ b/ntfy.xcodeproj/project.pbxproj @@ -542,7 +542,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - APP_BASE_URL = "http://192.168.105.215"; + APP_BASE_URL = "http://192.168.1.7"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = ntfy/ntfy.entitlements; @@ -578,7 +578,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - APP_BASE_URL = "http://192.168.105.215"; + APP_BASE_URL = "http://192.168.1.7"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = ntfy/ntfy.entitlements; diff --git a/ntfy/App/AppDelegate.swift b/ntfy/App/AppDelegate.swift index 697834b..64926d5 100644 --- a/ntfy/App/AppDelegate.swift +++ b/ntfy/App/AppDelegate.swift @@ -33,6 +33,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, ObservableObject { // Register too receive remote notifications application.registerForRemoteNotifications() + NotificationCenter.default.post(name: .shouldPoll, object: nil) return true } @@ -97,7 +98,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { ) { let userInfo = notification.request.content.userInfo Log.d(tag, "Notification received via userNotificationCenter(willPresent)", userInfo) - NotificationCenter.default.post(name: .notificationReceived, object: nil) + NotificationCenter.default.post(name: .shouldPoll, object: nil) completionHandler([[.banner, .sound]]) } @@ -130,7 +131,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { UIApplication.shared.open(url, options: [:], completionHandler: nil) } - NotificationCenter.default.post(name: .notificationReceived, object: nil) + NotificationCenter.default.post(name: .shouldPoll, object: nil) completionHandler() } } diff --git a/ntfy/Persistence/Store.swift b/ntfy/Persistence/Store.swift index 7b40d33..29e3677 100644 --- a/ntfy/Persistence/Store.swift +++ b/ntfy/Persistence/Store.swift @@ -49,7 +49,6 @@ class Store: ObservableObject { DispatchQueue.main.async { self.hardRefresh() - NotificationCenter.default.post(name: .notificationReceived, object: nil) } } .store(in: &cancellables) @@ -272,5 +271,5 @@ extension Store { } extension Foundation.Notification.Name { - static let notificationReceived = Foundation.Notification.Name("notificationReceived") + static let shouldPoll = Foundation.Notification.Name("shouldPoll") } diff --git a/ntfy/Views/NotificationListView.swift b/ntfy/Views/NotificationListView.swift index 4f3815b..993d8a1 100644 --- a/ntfy/Views/NotificationListView.swift +++ b/ntfy/Views/NotificationListView.swift @@ -24,18 +24,18 @@ struct NotificationListView: View { } var body: some View { - let notificationReceived = Foundation.Notification.Name("notificationReceived") + let shouldPoll = Foundation.Notification.Name("shouldPoll") if #available(iOS 15.0, *) { notificationList .refreshable { subscriptionManager.poll(subscription) - }.onReceive(NotificationCenter.default.publisher(for: notificationReceived)) { _ in + }.onReceive(NotificationCenter.default.publisher(for: shouldPoll)) { _ in // Handle the notification subscriptionManager.poll(subscription) } } else { - notificationList.onReceive(NotificationCenter.default.publisher(for: notificationReceived)) { _ in + notificationList.onReceive(NotificationCenter.default.publisher(for: shouldPoll)) { _ in // Handle the notification subscriptionManager.poll(subscription) } diff --git a/ntfy/Views/SubscriptionListView.swift b/ntfy/Views/SubscriptionListView.swift index 5bcd4bc..0f38afc 100644 --- a/ntfy/Views/SubscriptionListView.swift +++ b/ntfy/Views/SubscriptionListView.swift @@ -15,7 +15,7 @@ struct SubscriptionListView: View { } var body: some View { - let notificationReceived = Foundation.Notification.Name("notificationReceived") + let shouldPoll = Foundation.Notification.Name("shouldPoll") NavigationView { if #available(iOS 15.0, *) { @@ -24,7 +24,7 @@ struct SubscriptionListView: View { subscriptions.forEach { subscription in subscriptionManager.poll(subscription) } - }.onReceive(NotificationCenter.default.publisher(for: notificationReceived)) { _ in + }.onReceive(NotificationCenter.default.publisher(for: shouldPoll)) { _ in // Handle the notification subscriptions.forEach { subscription in subscriptionManager.poll(subscription) @@ -32,7 +32,7 @@ struct SubscriptionListView: View { } } else { subscriptionList - .onReceive(NotificationCenter.default.publisher(for: notificationReceived)) { _ in + .onReceive(NotificationCenter.default.publisher(for: shouldPoll)) { _ in // Handle the notification subscriptions.forEach { subscription in subscriptionManager.poll(subscription) From 697aa5169583472cabdfef8d97205bf12617f787 Mon Sep 17 00:00:00 2001 From: Tom Caputi Date: Tue, 31 Oct 2023 05:54:31 -0400 Subject: [PATCH 06/12] new implementation with observables --- ntfy.xcodeproj/project.pbxproj | 8 ++++ .../Persistence/NotificationsObservable.swift | 42 +++++++++++++++++++ ntfy/Persistence/Store.swift | 1 + ntfy/Persistence/SubscriptionManager.swift | 2 +- .../Persistence/SubscriptionsObservable.swift | 32 ++++++++++++++ ntfy/Views/NotificationListView.swift | 22 +++++----- ntfy/Views/SubscriptionListView.swift | 23 +++------- 7 files changed, 100 insertions(+), 30 deletions(-) create mode 100644 ntfy/Persistence/NotificationsObservable.swift create mode 100644 ntfy/Persistence/SubscriptionsObservable.swift diff --git a/ntfy.xcodeproj/project.pbxproj b/ntfy.xcodeproj/project.pbxproj index daad103..6dcd36a 100644 --- a/ntfy.xcodeproj/project.pbxproj +++ b/ntfy.xcodeproj/project.pbxproj @@ -50,6 +50,8 @@ 94CD196A283E666900973B93 /* EmojiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD1969283E666900973B93 /* EmojiManager.swift */; }; 94CD196B283E666900973B93 /* EmojiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD1969283E666900973B93 /* EmojiManager.swift */; }; 94E9196C28353E0100F30170 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F216283531A200CDE4DD /* Log.swift */; }; + E27008102AF0F64B006E33BA /* SubscriptionsObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E270080F2AF0F64B006E33BA /* SubscriptionsObservable.swift */; }; + E27008122AF1030A006E33BA /* NotificationsObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27008112AF1030A006E33BA /* NotificationsObservable.swift */; }; E278CB332AECECCA004B9143 /* QRScannerUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E278CB322AECECCA004B9143 /* QRScannerUIView.swift */; }; /* End PBXBuildFile section */ @@ -112,6 +114,8 @@ 94B736D6284AF9BE003D69FB /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; 94CD1965283E662900973B93 /* emojis.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = emojis.json; sourceTree = ""; }; 94CD1969283E666900973B93 /* EmojiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiManager.swift; sourceTree = ""; }; + E270080F2AF0F64B006E33BA /* SubscriptionsObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsObservable.swift; sourceTree = ""; }; + E27008112AF1030A006E33BA /* NotificationsObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsObservable.swift; sourceTree = ""; }; E278CB322AECECCA004B9143 /* QRScannerUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScannerUIView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -215,6 +219,8 @@ 9474F20B283321C300CDE4DD /* Notification.swift */, 94A3F7C7283734D900C48E79 /* SubscriptionManager.swift */, 9407EDD9284ADE1F00C1C334 /* User.swift */, + E270080F2AF0F64B006E33BA /* SubscriptionsObservable.swift */, + E27008112AF1030A006E33BA /* NotificationsObservable.swift */, ); path = Persistence; sourceTree = ""; @@ -364,6 +370,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E27008102AF0F64B006E33BA /* SubscriptionsObservable.swift in Sources */, 02024E60283D7CBB0064224A /* Extensions.swift in Sources */, 948671472841B0B20093C7A4 /* NotificationContent.swift in Sources */, 9474F1F92830835400CDE4DD /* Store.swift in Sources */, @@ -376,6 +383,7 @@ 9474F1C3282F2AA700CDE4DD /* ContentView.swift in Sources */, 9474F20C283321C300CDE4DD /* Notification.swift in Sources */, 9474F1F22830825600CDE4DD /* SubscriptionListView.swift in Sources */, + E27008122AF1030A006E33BA /* NotificationsObservable.swift in Sources */, 9486714A2841D0CE0093C7A4 /* ActionExecutor.swift in Sources */, 9474F1FD2831311A00CDE4DD /* SubscriptionAddView.swift in Sources */, E278CB332AECECCA004B9143 /* QRScannerUIView.swift in Sources */, diff --git a/ntfy/Persistence/NotificationsObservable.swift b/ntfy/Persistence/NotificationsObservable.swift new file mode 100644 index 0000000..f0ac200 --- /dev/null +++ b/ntfy/Persistence/NotificationsObservable.swift @@ -0,0 +1,42 @@ +import CoreData +import SwiftUI + +class NotificationsObservable: NSObject, ObservableObject { + + private var subscriptionID: NSManagedObjectID + private lazy var fetchedResultsController: NSFetchedResultsController = { + let fetchRequest: NSFetchRequest = Notification.fetchRequest() + + // Filter by the desired subscription + fetchRequest.predicate = NSPredicate(format: "subscription == %@", subscriptionID) + + // Sort descriptors if you need them + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)] // Assuming you have a 'date' attribute on the NotificationEntity + + let controller = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: Store.shared.context, sectionNameKeyPath: nil, cacheName: nil) + controller.delegate = self + return controller + }() + + @Published var notifications: [Notification] = [] + + init(subscriptionID: NSManagedObjectID) { + self.subscriptionID = subscriptionID + super.init() + + do { + try self.fetchedResultsController.performFetch() + self.notifications = self.fetchedResultsController.fetchedObjects ?? [] + } catch { + print("Failed to fetch notifications.") + } + } +} + +extension NotificationsObservable: NSFetchedResultsControllerDelegate { + func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + DispatchQueue.main.async { + self.notifications = self.fetchedResultsController.fetchedObjects ?? [] + } + } +} diff --git a/ntfy/Persistence/Store.swift b/ntfy/Persistence/Store.swift index 29e3677..369eaa8 100644 --- a/ntfy/Persistence/Store.swift +++ b/ntfy/Persistence/Store.swift @@ -80,6 +80,7 @@ class Store: ObservableObject { subscription.baseUrl = baseUrl subscription.topic = topic DispatchQueue.main.sync { + print("----------> SAVING SUBSCRIPTION \(topic)") try? context.save() } return subscription diff --git a/ntfy/Persistence/SubscriptionManager.swift b/ntfy/Persistence/SubscriptionManager.swift index 81560eb..8526a84 100644 --- a/ntfy/Persistence/SubscriptionManager.swift +++ b/ntfy/Persistence/SubscriptionManager.swift @@ -4,7 +4,7 @@ import FirebaseMessaging /// Manager to combine persisting a subscription to the data store and subscribing to Firebase. /// This is to centralize the logic in one place. struct SubscriptionManager { - private let tag = "Store" + private let tag = "SubscriptionManager" var store: Store func subscribe(baseUrl: String, topic: String) { diff --git a/ntfy/Persistence/SubscriptionsObservable.swift b/ntfy/Persistence/SubscriptionsObservable.swift new file mode 100644 index 0000000..3812185 --- /dev/null +++ b/ntfy/Persistence/SubscriptionsObservable.swift @@ -0,0 +1,32 @@ +import CoreData +import SwiftUI + +class SubscriptionsObservable: NSObject, ObservableObject { + private lazy var fetchedResultsController: NSFetchedResultsController = { + let fetchRequest: NSFetchRequest = Subscription.fetchRequest() + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "topic", ascending: true)] + + let controller = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: Store.shared.context, sectionNameKeyPath: nil, cacheName: nil) + controller.delegate = self + + do { + try controller.performFetch() + } catch { + print("Failed to fetch items: \(error)") + } + + return controller + }() + + var subscriptions: [Subscription] { + fetchedResultsController.fetchedObjects ?? [] + } +} + +extension SubscriptionsObservable: NSFetchedResultsControllerDelegate { + func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + DispatchQueue.main.async { + self.objectWillChange.send() + } + } +} diff --git a/ntfy/Views/NotificationListView.swift b/ntfy/Views/NotificationListView.swift index 993d8a1..70c9034 100644 --- a/ntfy/Views/NotificationListView.swift +++ b/ntfy/Views/NotificationListView.swift @@ -12,6 +12,7 @@ struct NotificationListView: View { @EnvironmentObject private var store: Store @ObservedObject var subscription: Subscription + @ObservedObject var notificationsModel: NotificationsObservable @State private var editMode = EditMode.inactive @State private var selection = Set() @@ -22,6 +23,11 @@ struct NotificationListView: View { private var subscriptionManager: SubscriptionManager { return SubscriptionManager(store: store) } + + init(subscription: Subscription) { + self.subscription = subscription + self.notificationsModel = NotificationsObservable(subscriptionID: subscription.objectID) + } var body: some View { let shouldPoll = Foundation.Notification.Name("shouldPoll") @@ -30,21 +36,15 @@ struct NotificationListView: View { notificationList .refreshable { subscriptionManager.poll(subscription) - }.onReceive(NotificationCenter.default.publisher(for: shouldPoll)) { _ in - // Handle the notification - subscriptionManager.poll(subscription) } } else { - notificationList.onReceive(NotificationCenter.default.publisher(for: shouldPoll)) { _ in - // Handle the notification - subscriptionManager.poll(subscription) - } + notificationList } } private var notificationList: some View { List(selection: $selection) { - ForEach(subscription.notificationsSorted(), id: \.self) { notification in + ForEach(notificationsModel.notifications, id: \.self) { notification in NotificationRowView(notification: notification) } } @@ -82,13 +82,13 @@ struct NotificationListView: View { subscriptionManager.poll(subscription) } } - if subscription.notificationCount() > 0 { + if notificationsModel.notifications.count > 0 { editButton } Button("Send test notification") { self.sendTestNotification() } - if subscription.notificationCount() > 0 { + if notificationsModel.notifications.count > 0 { Button("Clear all notifications") { self.showAlert = true self.activeAlert = .clear @@ -148,7 +148,7 @@ struct NotificationListView: View { } } .overlay(Group { - if subscription.notificationCount() == 0 { + if notificationsModel.notifications.count == 0 { VStack { Text("You haven't received any notifications for this topic yet.") .font(.title2) diff --git a/ntfy/Views/SubscriptionListView.swift b/ntfy/Views/SubscriptionListView.swift index 0f38afc..c948c86 100644 --- a/ntfy/Views/SubscriptionListView.swift +++ b/ntfy/Views/SubscriptionListView.swift @@ -7,7 +7,7 @@ struct SubscriptionListView: View { let tag = "SubscriptionList" @EnvironmentObject private var store: Store - @FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Subscription.topic, ascending: true)]) var subscriptions: FetchedResults + @ObservedObject var subscriptionsModel = SubscriptionsObservable() @State private var showingAddDialog = false private var subscriptionManager: SubscriptionManager { @@ -15,33 +15,20 @@ struct SubscriptionListView: View { } var body: some View { - let shouldPoll = Foundation.Notification.Name("shouldPoll") - NavigationView { if #available(iOS 15.0, *) { subscriptionList .refreshable { - subscriptions.forEach { subscription in - subscriptionManager.poll(subscription) - } - }.onReceive(NotificationCenter.default.publisher(for: shouldPoll)) { _ in - // Handle the notification - subscriptions.forEach { subscription in + subscriptionsModel.subscriptions.forEach { subscription in subscriptionManager.poll(subscription) } } } else { subscriptionList - .onReceive(NotificationCenter.default.publisher(for: shouldPoll)) { _ in - // Handle the notification - subscriptions.forEach { subscription in - subscriptionManager.poll(subscription) - } - } .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button { - subscriptions.forEach { subscription in + subscriptionsModel.subscriptions.forEach { subscription in subscriptionManager.poll(subscription) } } label: { @@ -56,7 +43,7 @@ struct SubscriptionListView: View { private var subscriptionList: some View { List { - ForEach(subscriptions) { subscription in + ForEach(subscriptionsModel.subscriptions) { subscription in SubscriptionItemNavView(subscription: subscription) } } @@ -72,7 +59,7 @@ struct SubscriptionListView: View { } } .overlay(Group { - if subscriptions.isEmpty { + if subscriptionsModel.subscriptions.isEmpty { VStack { Text("It looks like you don't have any subscriptions yet") .font(.title2) From 1c43d7f5c64d632a1d994a227e434014767789e0 Mon Sep 17 00:00:00 2001 From: Tom Caputi Date: Fri, 3 Nov 2023 03:10:16 -0400 Subject: [PATCH 07/12] all major cases seem to work now --- ntfy.xcodeproj/project.pbxproj | 4 +-- ntfy/App/AppDelegate.swift | 28 +++++++++++++++++-- ntfy/Persistence/Store.swift | 6 ++-- .../Persistence/SubscriptionsObservable.swift | 27 ++++++++++++++++++ ntfy/Views/NotificationListView.swift | 2 -- 5 files changed, 56 insertions(+), 11 deletions(-) diff --git a/ntfy.xcodeproj/project.pbxproj b/ntfy.xcodeproj/project.pbxproj index 6dcd36a..bbf38dc 100644 --- a/ntfy.xcodeproj/project.pbxproj +++ b/ntfy.xcodeproj/project.pbxproj @@ -550,7 +550,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - APP_BASE_URL = "http://192.168.1.7"; + APP_BASE_URL = "http://192.168.1.8"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = ntfy/ntfy.entitlements; @@ -586,7 +586,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - APP_BASE_URL = "http://192.168.1.7"; + APP_BASE_URL = "http://192.168.1.8"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = ntfy/ntfy.entitlements; diff --git a/ntfy/App/AppDelegate.swift b/ntfy/App/AppDelegate.swift index 64926d5..51b5433 100644 --- a/ntfy/App/AppDelegate.swift +++ b/ntfy/App/AppDelegate.swift @@ -33,7 +33,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, ObservableObject { // Register too receive remote notifications application.registerForRemoteNotifications() - NotificationCenter.default.post(name: .shouldPoll, object: nil) return true } @@ -98,7 +97,23 @@ extension AppDelegate: UNUserNotificationCenterDelegate { ) { let userInfo = notification.request.content.userInfo Log.d(tag, "Notification received via userNotificationCenter(willPresent)", userInfo) - NotificationCenter.default.post(name: .shouldPoll, object: nil) + + guard let message = Message.from(userInfo: userInfo) else { + Log.w(tag, "Cannot convert userInfo to message", userInfo) + completionHandler([]) + return + } + + let store = Store.shared + let baseUrl = userInfo["base_url"] as? String ?? Config.appBaseUrl + + guard let subscription = store.getSubscription(baseUrl: baseUrl, topic: message.topic) else { + Log.w(tag, "Subscription \(topicUrl(baseUrl: baseUrl, topic: message.topic)) unknown") + completionHandler([]) + return + } + Store.shared.save(notificationFromMessage: message, withSubscription: subscription) + completionHandler([[.banner, .sound]]) } @@ -116,8 +131,16 @@ extension AppDelegate: UNUserNotificationCenterDelegate { return } + let store = Store.shared let baseUrl = userInfo["base_url"] as? String ?? Config.appBaseUrl let action = message.actions?.first { $0.id == response.actionIdentifier } + + guard let subscription = store.getSubscription(baseUrl: baseUrl, topic: message.topic) else { + Log.w(tag, "Subscription \(topicUrl(baseUrl: baseUrl, topic: message.topic)) unknown") + completionHandler() + return + } + Store.shared.save(notificationFromMessage: message, withSubscription: subscription) // Show current topic if message.topic != "" { @@ -131,7 +154,6 @@ extension AppDelegate: UNUserNotificationCenterDelegate { UIApplication.shared.open(url, options: [:], completionHandler: nil) } - NotificationCenter.default.post(name: .shouldPoll, object: nil) completionHandler() } } diff --git a/ntfy/Persistence/Store.swift b/ntfy/Persistence/Store.swift index 369eaa8..80d9b8b 100644 --- a/ntfy/Persistence/Store.swift +++ b/ntfy/Persistence/Store.swift @@ -118,8 +118,10 @@ class Store: ObservableObject { notification.tags = message.tags?.joined(separator: ",") ?? "" notification.actions = Actions.shared.encode(message.actions) notification.click = message.click ?? "" + notification.subscription = subscription subscription.addToNotifications(notification) subscription.lastNotificationId = message.id + print("--------> STORING NOTIFICATION") try context.save() } catch let error { Log.w(Store.tag, "Cannot store notification (fromMessage)", error) @@ -270,7 +272,3 @@ extension Store { return notification } } - -extension Foundation.Notification.Name { - static let shouldPoll = Foundation.Notification.Name("shouldPoll") -} diff --git a/ntfy/Persistence/SubscriptionsObservable.swift b/ntfy/Persistence/SubscriptionsObservable.swift index 3812185..187cae3 100644 --- a/ntfy/Persistence/SubscriptionsObservable.swift +++ b/ntfy/Persistence/SubscriptionsObservable.swift @@ -2,6 +2,14 @@ import CoreData import SwiftUI class SubscriptionsObservable: NSObject, ObservableObject { + + override init() { + super.init() + + // This will force the initialization of notificationsFetchedResultsController + _ = self.notificationsFetchedResultsController + } + private lazy var fetchedResultsController: NSFetchedResultsController = { let fetchRequest: NSFetchRequest = Subscription.fetchRequest() fetchRequest.sortDescriptors = [NSSortDescriptor(key: "topic", ascending: true)] @@ -10,6 +18,7 @@ class SubscriptionsObservable: NSObject, ObservableObject { controller.delegate = self do { + print("-----------> FETCHING SUBSCRIPTIONS") try controller.performFetch() } catch { print("Failed to fetch items: \(error)") @@ -18,6 +27,23 @@ class SubscriptionsObservable: NSObject, ObservableObject { return controller }() + private lazy var notificationsFetchedResultsController: NSFetchedResultsController = { + let fetchRequest: NSFetchRequest = Notification.fetchRequest() + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "time", ascending: true)] + + let controller = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: Store.shared.context, sectionNameKeyPath: nil, cacheName: nil) + controller.delegate = self + + do { + print("-----------> FETCHING NOTIFICATIONS") + try controller.performFetch() + } catch { + print("Failed to fetch notifications: \(error)") + } + + return controller + }() + var subscriptions: [Subscription] { fetchedResultsController.fetchedObjects ?? [] } @@ -25,6 +51,7 @@ class SubscriptionsObservable: NSObject, ObservableObject { extension SubscriptionsObservable: NSFetchedResultsControllerDelegate { func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + print("-----------> FETCHING NOTIFICATIONS") DispatchQueue.main.async { self.objectWillChange.send() } diff --git a/ntfy/Views/NotificationListView.swift b/ntfy/Views/NotificationListView.swift index 70c9034..b4d94c3 100644 --- a/ntfy/Views/NotificationListView.swift +++ b/ntfy/Views/NotificationListView.swift @@ -30,8 +30,6 @@ struct NotificationListView: View { } var body: some View { - let shouldPoll = Foundation.Notification.Name("shouldPoll") - if #available(iOS 15.0, *) { notificationList .refreshable { From ea26378e291f056915c6e0af7f09c092a0af91a9 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Sun, 5 Nov 2023 14:00:28 -0500 Subject: [PATCH 08/12] UI refresh fixes, and QR code --- ntfy.xcodeproj/project.pbxproj | 22 ++++++++----------- .../xcshareddata/xcschemes/ntfyNSE.xcscheme | 3 +-- ntfy/App/AppDelegate.swift | 17 ++++++++++++-- ntfy/App/AppMain.swift | 2 +- .../Persistence/NotificationsObservable.swift | 5 +++-- ntfy/Persistence/Store.swift | 6 ++--- .../Persistence/SubscriptionsObservable.swift | 11 +++++----- ntfy/Utils/Log.swift | 2 +- ntfy/{Utils => Views}/QRScannerUIView.swift | 0 ntfy/ntfy.entitlements | 2 +- ntfyNSE/ntfyNSE.entitlements | 2 +- 11 files changed, 41 insertions(+), 31 deletions(-) rename ntfy/{Utils => Views}/QRScannerUIView.swift (100%) diff --git a/ntfy.xcodeproj/project.pbxproj b/ntfy.xcodeproj/project.pbxproj index bbf38dc..dd2dc24 100644 --- a/ntfy.xcodeproj/project.pbxproj +++ b/ntfy.xcodeproj/project.pbxproj @@ -206,6 +206,7 @@ 94B736D4284AF9B2003D69FB /* SettingsView.swift */, 94B736D6284AF9BE003D69FB /* MainView.swift */, 9474F20728331F3900CDE4DD /* NotificationListView.swift */, + E278CB322AECECCA004B9143 /* QRScannerUIView.swift */, ); path = Views; sourceTree = ""; @@ -245,7 +246,6 @@ 94867142283EC9950093C7A4 /* Actions.swift */, 948671462841B0B20093C7A4 /* NotificationContent.swift */, 948671492841D0CE0093C7A4 /* ActionExecutor.swift */, - E278CB322AECECCA004B9143 /* QRScannerUIView.swift */, ); path = Utils; sourceTree = ""; @@ -550,14 +550,13 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - APP_BASE_URL = "http://192.168.1.8"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = ntfy/ntfy.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_ASSET_PATHS = "\"ntfy/Assets/Preview Content\""; - DEVELOPMENT_TEAM = MZWHX5Z44T; + DEVELOPMENT_TEAM = YXQ4AMS4B4; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ntfy/Info.plist; @@ -574,7 +573,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.2; - PRODUCT_BUNDLE_IDENTIFIER = com.tcaputi.ntfy; + PRODUCT_BUNDLE_IDENTIFIER = io.heckel.ntfy; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -586,14 +585,13 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - APP_BASE_URL = "http://192.168.1.8"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = ntfy/ntfy.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_ASSET_PATHS = "\"ntfy/Assets/Preview Content\""; - DEVELOPMENT_TEAM = MZWHX5Z44T; + DEVELOPMENT_TEAM = YXQ4AMS4B4; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ntfy/Info.plist; @@ -610,7 +608,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.2; - PRODUCT_BUNDLE_IDENTIFIER = com.tcaputi.ntfy; + PRODUCT_BUNDLE_IDENTIFIER = io.heckel.ntfy; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -621,11 +619,10 @@ 9474F1ED282F3FFD00CDE4DD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - APP_BASE_URL = "http://192.168.1.7"; CODE_SIGN_ENTITLEMENTS = ntfyNSE/ntfyNSE.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 3; - DEVELOPMENT_TEAM = MZWHX5Z44T; + DEVELOPMENT_TEAM = YXQ4AMS4B4; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ntfyNSE/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ntfyNSE; @@ -637,7 +634,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.2; - PRODUCT_BUNDLE_IDENTIFIER = com.tcaputi.ntfy.ntfyNSE; + PRODUCT_BUNDLE_IDENTIFIER = io.heckel.ntfy.ntfyNSE; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; @@ -649,11 +646,10 @@ 9474F1EE282F3FFD00CDE4DD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - APP_BASE_URL = "http://192.168.1.7"; CODE_SIGN_ENTITLEMENTS = ntfyNSE/ntfyNSE.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 3; - DEVELOPMENT_TEAM = MZWHX5Z44T; + DEVELOPMENT_TEAM = YXQ4AMS4B4; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ntfyNSE/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ntfyNSE; @@ -665,7 +661,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.2; - PRODUCT_BUNDLE_IDENTIFIER = com.tcaputi.ntfy.ntfyNSE; + PRODUCT_BUNDLE_IDENTIFIER = io.heckel.ntfy.ntfyNSE; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/ntfy.xcodeproj/xcshareddata/xcschemes/ntfyNSE.xcscheme b/ntfy.xcodeproj/xcshareddata/xcschemes/ntfyNSE.xcscheme index 7a4f970..16cd027 100644 --- a/ntfy.xcodeproj/xcshareddata/xcschemes/ntfyNSE.xcscheme +++ b/ntfy.xcodeproj/xcshareddata/xcschemes/ntfyNSE.xcscheme @@ -58,7 +58,7 @@ launchAutomaticallySubstyle = "2"> @@ -77,7 +77,6 @@ savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES" - askForAppToLaunch = "Yes" launchAutomaticallySubstyle = "2"> diff --git a/ntfy/App/AppDelegate.swift b/ntfy/App/AppDelegate.swift index 51b5433..4758ec3 100644 --- a/ntfy/App/AppDelegate.swift +++ b/ntfy/App/AppDelegate.swift @@ -161,8 +161,21 @@ extension AppDelegate: UNUserNotificationCenterDelegate { extension AppDelegate: MessagingDelegate { func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { Log.d(tag, "Firebase token received: \(String(describing: fcmToken))") - - // We wait until we have a registration token before subscribing to our pollTopic + + // Subscribe to ~poll topic Messaging.messaging().subscribe(toTopic: pollTopic) + + // Re-subscribe to Firebase for all topics + let store = Store.shared + store.getSubscriptions()?.forEach{ subscription in + if let baseUrl = subscription.baseUrl, let topic = subscription.topic { + Log.d(tag, "Re-subscribing to topic \(baseUrl)/\(topic)") + if baseUrl == Config.appBaseUrl { + Messaging.messaging().subscribe(toTopic: topic) + } else { + Messaging.messaging().subscribe(toTopic: topicHash(baseUrl: baseUrl, topic: topic)) + } + } + } } } diff --git a/ntfy/App/AppMain.swift b/ntfy/App/AppMain.swift index 1fedf39..beb25f3 100644 --- a/ntfy/App/AppMain.swift +++ b/ntfy/App/AppMain.swift @@ -5,7 +5,7 @@ import Firebase @main struct AppMain: App { - private let tag = "main" + private let tag = "AppMain" @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate: AppDelegate @StateObject private var store = Store.shared diff --git a/ntfy/Persistence/NotificationsObservable.swift b/ntfy/Persistence/NotificationsObservable.swift index f0ac200..5b71507 100644 --- a/ntfy/Persistence/NotificationsObservable.swift +++ b/ntfy/Persistence/NotificationsObservable.swift @@ -2,8 +2,9 @@ import CoreData import SwiftUI class NotificationsObservable: NSObject, ObservableObject { - + private let tag = "NotificationsObservable" private var subscriptionID: NSManagedObjectID + private lazy var fetchedResultsController: NSFetchedResultsController = { let fetchRequest: NSFetchRequest = Notification.fetchRequest() @@ -28,7 +29,7 @@ class NotificationsObservable: NSObject, ObservableObject { try self.fetchedResultsController.performFetch() self.notifications = self.fetchedResultsController.fetchedObjects ?? [] } catch { - print("Failed to fetch notifications.") + Log.w(tag, "Failed to fetch notifications \(error)") } } } diff --git a/ntfy/Persistence/Store.swift b/ntfy/Persistence/Store.swift index 80d9b8b..afed030 100644 --- a/ntfy/Persistence/Store.swift +++ b/ntfy/Persistence/Store.swift @@ -7,7 +7,7 @@ import Combine class Store: ObservableObject { static let shared = Store() static let tag = "Store" - static let appGroup = "group.com.tcaputi.ntfy" // Must match app group of ntfy = ntfyNSE targets + static let appGroup = "group.io.heckel.ntfy" // Must match app group of ntfy = ntfyNSE targets static let modelName = "ntfy" // Must match .xdatamodeld folder static let prefKeyDefaultBaseUrl = "defaultBaseUrl" @@ -80,7 +80,7 @@ class Store: ObservableObject { subscription.baseUrl = baseUrl subscription.topic = topic DispatchQueue.main.sync { - print("----------> SAVING SUBSCRIPTION \(topic)") + Log.d(Store.tag, "Storing subscription baseUrl=\(baseUrl), topic=\(topic)") try? context.save() } return subscription @@ -121,7 +121,7 @@ class Store: ObservableObject { notification.subscription = subscription subscription.addToNotifications(notification) subscription.lastNotificationId = message.id - print("--------> STORING NOTIFICATION") + Log.d(Store.tag, "Storing notifcation with ID \(notification.id ?? "")") try context.save() } catch let error { Log.w(Store.tag, "Cannot store notification (fromMessage)", error) diff --git a/ntfy/Persistence/SubscriptionsObservable.swift b/ntfy/Persistence/SubscriptionsObservable.swift index 187cae3..9e432d3 100644 --- a/ntfy/Persistence/SubscriptionsObservable.swift +++ b/ntfy/Persistence/SubscriptionsObservable.swift @@ -2,6 +2,7 @@ import CoreData import SwiftUI class SubscriptionsObservable: NSObject, ObservableObject { + private let tag = "SubscriptionsObservable" override init() { super.init() @@ -18,10 +19,10 @@ class SubscriptionsObservable: NSObject, ObservableObject { controller.delegate = self do { - print("-----------> FETCHING SUBSCRIPTIONS") + Log.d(tag, "Fetching subscriptions") try controller.performFetch() } catch { - print("Failed to fetch items: \(error)") + Log.w(tag, "Failed to fetch items: \(error)", error) } return controller @@ -35,10 +36,10 @@ class SubscriptionsObservable: NSObject, ObservableObject { controller.delegate = self do { - print("-----------> FETCHING NOTIFICATIONS") + Log.d(tag, "Fetching notifications") try controller.performFetch() } catch { - print("Failed to fetch notifications: \(error)") + Log.w(tag, "Failed to fetch notifications: \(error)", error) } return controller @@ -51,7 +52,7 @@ class SubscriptionsObservable: NSObject, ObservableObject { extension SubscriptionsObservable: NSFetchedResultsControllerDelegate { func controllerDidChangeContent(_ controller: NSFetchedResultsController) { - print("-----------> FETCHING NOTIFICATIONS") + Log.d(tag, "Fetching notifications") DispatchQueue.main.async { self.objectWillChange.send() } diff --git a/ntfy/Utils/Log.swift b/ntfy/Utils/Log.swift index 87f7cf4..35adf2f 100644 --- a/ntfy/Utils/Log.swift +++ b/ntfy/Utils/Log.swift @@ -1,7 +1,7 @@ import Foundation struct Log { - private static let dateFormat = "yyyy-MM-dd hh:mm:ss.SSSSSSZ" + private static let dateFormat = "yy-MM-dd hh:mm:ss.SSS" private static let dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = dateFormat diff --git a/ntfy/Utils/QRScannerUIView.swift b/ntfy/Views/QRScannerUIView.swift similarity index 100% rename from ntfy/Utils/QRScannerUIView.swift rename to ntfy/Views/QRScannerUIView.swift diff --git a/ntfy/ntfy.entitlements b/ntfy/ntfy.entitlements index d16d692..384fbad 100644 --- a/ntfy/ntfy.entitlements +++ b/ntfy/ntfy.entitlements @@ -6,7 +6,7 @@ development com.apple.security.application-groups - group.com.tcaputi.ntfy + group.io.heckel.ntfy diff --git a/ntfyNSE/ntfyNSE.entitlements b/ntfyNSE/ntfyNSE.entitlements index d16d692..384fbad 100644 --- a/ntfyNSE/ntfyNSE.entitlements +++ b/ntfyNSE/ntfyNSE.entitlements @@ -6,7 +6,7 @@ development com.apple.security.application-groups - group.com.tcaputi.ntfy + group.io.heckel.ntfy From 20804eacc64f13b4e4f75a39c9a058f91541f523 Mon Sep 17 00:00:00 2001 From: "Philipp C. Heckel" Date: Mon, 13 Nov 2023 11:35:59 -0500 Subject: [PATCH 09/12] Update ntfy/Persistence/Store.swift Co-authored-by: Arun Philip --- ntfy/Persistence/Store.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ntfy/Persistence/Store.swift b/ntfy/Persistence/Store.swift index afed030..c01fe8e 100644 --- a/ntfy/Persistence/Store.swift +++ b/ntfy/Persistence/Store.swift @@ -121,7 +121,7 @@ class Store: ObservableObject { notification.subscription = subscription subscription.addToNotifications(notification) subscription.lastNotificationId = message.id - Log.d(Store.tag, "Storing notifcation with ID \(notification.id ?? "")") + Log.d(Store.tag, "Storing notification with ID \(notification.id ?? "")") try context.save() } catch let error { Log.w(Store.tag, "Cannot store notification (fromMessage)", error) From de92923604e0e1128be3466853a3ef874dd2ae85 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Mon, 13 Nov 2023 11:49:16 -0500 Subject: [PATCH 10/12] Remove QR code stuff for now --- ntfy.xcodeproj/project.pbxproj | 12 +--- ntfy/Info.plist | 6 +- ntfy/Views/QRScannerUIView.swift | 84 ---------------------------- ntfy/Views/SubscriptionAddView.swift | 81 ++++----------------------- 4 files changed, 15 insertions(+), 168 deletions(-) delete mode 100644 ntfy/Views/QRScannerUIView.swift diff --git a/ntfy.xcodeproj/project.pbxproj b/ntfy.xcodeproj/project.pbxproj index dd2dc24..6737885 100644 --- a/ntfy.xcodeproj/project.pbxproj +++ b/ntfy.xcodeproj/project.pbxproj @@ -52,7 +52,6 @@ 94E9196C28353E0100F30170 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F216283531A200CDE4DD /* Log.swift */; }; E27008102AF0F64B006E33BA /* SubscriptionsObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E270080F2AF0F64B006E33BA /* SubscriptionsObservable.swift */; }; E27008122AF1030A006E33BA /* NotificationsObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27008112AF1030A006E33BA /* NotificationsObservable.swift */; }; - E278CB332AECECCA004B9143 /* QRScannerUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E278CB322AECECCA004B9143 /* QRScannerUIView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -116,7 +115,6 @@ 94CD1969283E666900973B93 /* EmojiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiManager.swift; sourceTree = ""; }; E270080F2AF0F64B006E33BA /* SubscriptionsObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsObservable.swift; sourceTree = ""; }; E27008112AF1030A006E33BA /* NotificationsObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsObservable.swift; sourceTree = ""; }; - E278CB322AECECCA004B9143 /* QRScannerUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScannerUIView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -206,7 +204,6 @@ 94B736D4284AF9B2003D69FB /* SettingsView.swift */, 94B736D6284AF9BE003D69FB /* MainView.swift */, 9474F20728331F3900CDE4DD /* NotificationListView.swift */, - E278CB322AECECCA004B9143 /* QRScannerUIView.swift */, ); path = Views; sourceTree = ""; @@ -218,10 +215,10 @@ 9474F1FE28316ACE00CDE4DD /* Subscription.swift */, 9474F1F82830835400CDE4DD /* Store.swift */, 9474F20B283321C300CDE4DD /* Notification.swift */, - 94A3F7C7283734D900C48E79 /* SubscriptionManager.swift */, - 9407EDD9284ADE1F00C1C334 /* User.swift */, - E270080F2AF0F64B006E33BA /* SubscriptionsObservable.swift */, E27008112AF1030A006E33BA /* NotificationsObservable.swift */, + 94A3F7C7283734D900C48E79 /* SubscriptionManager.swift */, + E270080F2AF0F64B006E33BA /* SubscriptionsObservable.swift */, + 9407EDD9284ADE1F00C1C334 /* User.swift */, ); path = Persistence; sourceTree = ""; @@ -386,7 +383,6 @@ E27008122AF1030A006E33BA /* NotificationsObservable.swift in Sources */, 9486714A2841D0CE0093C7A4 /* ActionExecutor.swift in Sources */, 9474F1FD2831311A00CDE4DD /* SubscriptionAddView.swift in Sources */, - E278CB332AECECCA004B9143 /* QRScannerUIView.swift in Sources */, 9474F1FF28316ACE00CDE4DD /* Subscription.swift in Sources */, 94CD196A283E666900973B93 /* EmojiManager.swift in Sources */, 9474F1C1282F2AA700CDE4DD /* AppMain.swift in Sources */, @@ -561,7 +557,6 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ntfy/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ntfy; - INFOPLIST_KEY_NSCameraUsageDescription = "We need access to the camera to scan QR codes."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; @@ -596,7 +591,6 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ntfy/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ntfy; - INFOPLIST_KEY_NSCameraUsageDescription = "We need access to the camera to scan QR codes."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; diff --git a/ntfy/Info.plist b/ntfy/Info.plist index 0ad242b..428685b 100644 --- a/ntfy/Info.plist +++ b/ntfy/Info.plist @@ -4,6 +4,8 @@ AppBaseURL $(APP_BASE_URL) + FirebaseAppDelegateProxyEnabled + NSAppTransportSecurity NSAllowsArbitraryLoads @@ -18,9 +20,5 @@ remote-notification - FirebaseAppDelegateProxyEnabled - - NSCameraUsageDescription - We need access to the camera for QR code scanning. diff --git a/ntfy/Views/QRScannerUIView.swift b/ntfy/Views/QRScannerUIView.swift deleted file mode 100644 index 9ecf8d2..0000000 --- a/ntfy/Views/QRScannerUIView.swift +++ /dev/null @@ -1,84 +0,0 @@ -import AVFoundation -import SwiftUI - -struct QRScannerUIView: UIViewRepresentable { - var onCodeDetected: (String) -> Void - - func makeUIView(context: Context) -> some UIView { - let view = QRScannerUIViewContainer() - - let captureSession = AVCaptureSession() - - guard let videoCaptureDevice = AVCaptureDevice.default(for: .video), - let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice), - captureSession.canAddInput(videoInput) else { - return view - } - captureSession.addInput(videoInput) - - let metadataOutput = AVCaptureMetadataOutput() - captureSession.addOutput(metadataOutput) - - metadataOutput.setMetadataObjectsDelegate(context.coordinator, queue: DispatchQueue.main) - metadataOutput.metadataObjectTypes = [.qr] - - let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) - previewLayer.frame = view.layer.bounds - previewLayer.videoGravity = .resizeAspectFill - view.layer.insertSublayer(previewLayer, at: 0) - - // Move the startRunning call to a background thread - DispatchQueue.global(qos: .userInitiated).async { - captureSession.startRunning() - } - - view.previewLayer = previewLayer - view.captureSession = captureSession - return view - } - - func updateUIView(_ uiView: UIViewType, context: Context) {} - - func makeCoordinator() -> Coordinator { - Coordinator(onCodeDetected: onCodeDetected) - } - - class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate { - var onCodeDetected: (String) -> Void - private var lastScanDate: Date? - private let debounceInterval: TimeInterval = 3.0 - - init(onCodeDetected: @escaping (String) -> Void) { - self.onCodeDetected = onCodeDetected - } - - func qrCodeScanned(_ code: String) { - let now = Date() - - // If it's the first scan or the interval since the last scan is more than the debounce interval - if let lastScan = lastScanDate, now.timeIntervalSince(lastScan) < debounceInterval { - return - } - - onCodeDetected(code) - lastScanDate = now - } - - func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { - if let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject, let stringValue = metadataObject.stringValue { - qrCodeScanned(stringValue) - } - } - } -} - - -class QRScannerUIViewContainer: UIView { - var previewLayer: AVCaptureVideoPreviewLayer? - var captureSession: AVCaptureSession? - - override func layoutSubviews() { - super.layoutSubviews() - previewLayer?.frame = self.bounds - } -} diff --git a/ntfy/Views/SubscriptionAddView.swift b/ntfy/Views/SubscriptionAddView.swift index 8b6b4cb..2fc8645 100644 --- a/ntfy/Views/SubscriptionAddView.swift +++ b/ntfy/Views/SubscriptionAddView.swift @@ -1,5 +1,4 @@ import SwiftUI -import AVFoundation struct SubscriptionAddView: View { private let tag = "SubscriptionAddView" @@ -18,7 +17,6 @@ struct SubscriptionAddView: View { @State private var loading = false @State private var addError: String? @State private var loginError: String? - @State private var hasCameraPermission: Bool = false private var subscriptionManager: SubscriptionManager { return SubscriptionManager(store: store) @@ -26,29 +24,19 @@ struct SubscriptionAddView: View { var body: some View { NavigationView { - VStack { - addView - // TODO: hide this if permission not granted - QRScannerUIView { code in - onQRCodeScanned(text: code) - } - .frame(height: 250) // You can adjust the height as needed. - .padding() - .onAppear(perform: checkCameraPermission) - } - // This is a little weird, but it works. The nagivation link for the login view - // is rendered in the background (it's hidden), abd we toggle it manually. + // is rendered in the backgroun (it's hidden), abd we toggle it manually. // If anyone has a better way to do a two-page layout let me know. - .background(Group { - NavigationLink( - destination: loginView, - isActive: $showLogin - ) { - EmptyView() - } - }) + addView + .background(Group { + NavigationLink( + destination: loginView, + isActive: $showLogin + ) { + EmptyView() + } + }) } } @@ -141,55 +129,6 @@ struct SubscriptionAddView: View { return topic.trimmingCharacters(in: .whitespaces) } - private func checkCameraPermission() { - switch AVCaptureDevice.authorizationStatus(for: .video) { - case .authorized: - self.hasCameraPermission = true - - case .notDetermined: - AVCaptureDevice.requestAccess(for: .video) { granted in - DispatchQueue.main.async { - self.hasCameraPermission = granted - if !granted { - self.hasCameraPermission = false - } - } - } - - case .denied, .restricted: - self.hasCameraPermission = false - - @unknown default: - break - } - } - - func onQRCodeScanned(text: String){ - // Check if the text is a valid URL with HTTP or HTTPS scheme - guard let url = URL(string: text), let scheme = url.scheme, ["http", "https"].contains(scheme) else { - return - } - - // Extract the base URL without the path - var components = URLComponents(url: url, resolvingAgainstBaseURL: false) - components?.path = "" - guard let foundBaseUrl = components?.url else { - return - } - - // Extract the route from the original URL - baseUrl = foundBaseUrl.absoluteString - useAnother = baseUrl != store.getDefaultBaseUrl() - - topic = url.path - if (topic.hasPrefix("/")) { - topic.removeFirst() - } - - print("------> \(baseUrl) : \(topic) : \(useAnother)") - subscribeOrShowLoginAction() - } - private func isAddViewValid() -> Bool { if sanitizedTopic.isEmpty { return false From 080243ae1747a7e8c1412b40f2adacaba66fdc19 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Tue, 14 Nov 2023 16:00:48 -0500 Subject: [PATCH 11/12] Remove Store.save calls --- ntfy/App/AppDelegate.swift | 25 ------------------- .../Persistence/NotificationsObservable.swift | 1 + .../Persistence/SubscriptionsObservable.swift | 2 +- 3 files changed, 2 insertions(+), 26 deletions(-) diff --git a/ntfy/App/AppDelegate.swift b/ntfy/App/AppDelegate.swift index 4758ec3..2862acf 100644 --- a/ntfy/App/AppDelegate.swift +++ b/ntfy/App/AppDelegate.swift @@ -97,23 +97,6 @@ extension AppDelegate: UNUserNotificationCenterDelegate { ) { let userInfo = notification.request.content.userInfo Log.d(tag, "Notification received via userNotificationCenter(willPresent)", userInfo) - - guard let message = Message.from(userInfo: userInfo) else { - Log.w(tag, "Cannot convert userInfo to message", userInfo) - completionHandler([]) - return - } - - let store = Store.shared - let baseUrl = userInfo["base_url"] as? String ?? Config.appBaseUrl - - guard let subscription = store.getSubscription(baseUrl: baseUrl, topic: message.topic) else { - Log.w(tag, "Subscription \(topicUrl(baseUrl: baseUrl, topic: message.topic)) unknown") - completionHandler([]) - return - } - Store.shared.save(notificationFromMessage: message, withSubscription: subscription) - completionHandler([[.banner, .sound]]) } @@ -131,17 +114,9 @@ extension AppDelegate: UNUserNotificationCenterDelegate { return } - let store = Store.shared let baseUrl = userInfo["base_url"] as? String ?? Config.appBaseUrl let action = message.actions?.first { $0.id == response.actionIdentifier } - guard let subscription = store.getSubscription(baseUrl: baseUrl, topic: message.topic) else { - Log.w(tag, "Subscription \(topicUrl(baseUrl: baseUrl, topic: message.topic)) unknown") - completionHandler() - return - } - Store.shared.save(notificationFromMessage: message, withSubscription: subscription) - // Show current topic if message.topic != "" { selectedBaseUrl = topicUrl(baseUrl: baseUrl, topic: message.topic) diff --git a/ntfy/Persistence/NotificationsObservable.swift b/ntfy/Persistence/NotificationsObservable.swift index 5b71507..7fea8bf 100644 --- a/ntfy/Persistence/NotificationsObservable.swift +++ b/ntfy/Persistence/NotificationsObservable.swift @@ -26,6 +26,7 @@ class NotificationsObservable: NSObject, ObservableObject { super.init() do { + Log.d(tag, "Fetching notifications") try self.fetchedResultsController.performFetch() self.notifications = self.fetchedResultsController.fetchedObjects ?? [] } catch { diff --git a/ntfy/Persistence/SubscriptionsObservable.swift b/ntfy/Persistence/SubscriptionsObservable.swift index 9e432d3..8a7aa1b 100644 --- a/ntfy/Persistence/SubscriptionsObservable.swift +++ b/ntfy/Persistence/SubscriptionsObservable.swift @@ -22,7 +22,7 @@ class SubscriptionsObservable: NSObject, ObservableObject { Log.d(tag, "Fetching subscriptions") try controller.performFetch() } catch { - Log.w(tag, "Failed to fetch items: \(error)", error) + Log.w(tag, "Failed to fetch subscriptions: \(error)", error) } return controller From 097f97ed68045a4cd059f76eb97740e3ca0e63a6 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Mon, 27 Nov 2023 21:50:17 -0500 Subject: [PATCH 12/12] BUmp --- ntfy.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ntfy.xcodeproj/project.pbxproj b/ntfy.xcodeproj/project.pbxproj index 6737885..3fcf1fa 100644 --- a/ntfy.xcodeproj/project.pbxproj +++ b/ntfy.xcodeproj/project.pbxproj @@ -550,7 +550,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = ntfy/ntfy.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_ASSET_PATHS = "\"ntfy/Assets/Preview Content\""; DEVELOPMENT_TEAM = YXQ4AMS4B4; ENABLE_PREVIEWS = YES; @@ -567,7 +567,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2; + MARKETING_VERSION = 1.3; PRODUCT_BUNDLE_IDENTIFIER = io.heckel.ntfy; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -584,7 +584,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = ntfy/ntfy.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_ASSET_PATHS = "\"ntfy/Assets/Preview Content\""; DEVELOPMENT_TEAM = YXQ4AMS4B4; ENABLE_PREVIEWS = YES; @@ -601,7 +601,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2; + MARKETING_VERSION = 1.3; PRODUCT_BUNDLE_IDENTIFIER = io.heckel.ntfy; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES;