Merge pull request #18 from binwiederhier/fixes-only

View fixes only
This commit is contained in:
Philipp C. Heckel 2023-11-27 21:51:27 -05:00 committed by GitHub
commit 3feb27848a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 430 additions and 100 deletions

View file

@ -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 */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -111,6 +113,8 @@
94B736D6284AF9BE003D69FB /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
94CD1965283E662900973B93 /* emojis.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = emojis.json; sourceTree = "<group>"; };
94CD1969283E666900973B93 /* EmojiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiManager.swift; sourceTree = "<group>"; };
E270080F2AF0F64B006E33BA /* SubscriptionsObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsObservable.swift; sourceTree = "<group>"; };
E27008112AF1030A006E33BA /* NotificationsObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsObservable.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -211,7 +215,9 @@
9474F1FE28316ACE00CDE4DD /* Subscription.swift */,
9474F1F82830835400CDE4DD /* Store.swift */,
9474F20B283321C300CDE4DD /* Notification.swift */,
E27008112AF1030A006E33BA /* NotificationsObservable.swift */,
94A3F7C7283734D900C48E79 /* SubscriptionManager.swift */,
E270080F2AF0F64B006E33BA /* SubscriptionsObservable.swift */,
9407EDD9284ADE1F00C1C334 /* User.swift */,
);
path = Persistence;
@ -361,6 +367,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 */,
@ -373,6 +380,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 */,
9474F1FF28316ACE00CDE4DD /* Subscription.swift in Sources */,
@ -542,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;
@ -559,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;
@ -576,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;
@ -593,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;

View file

@ -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"
}
},
{

View file

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "9474F1BC282F2AA700CDE4DD"
BuildableName = "ntfy.app"
BlueprintName = "ntfy"
ReferencedContainer = "container:ntfy.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "9474F1BC282F2AA700CDE4DD"
BuildableName = "ntfy.app"
BlueprintName = "ntfy"
ReferencedContainer = "container:ntfy.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "9474F1BC282F2AA700CDE4DD"
BuildableName = "ntfy.app"
BlueprintName = "ntfy"
ReferencedContainer = "container:ntfy.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "9474F1E3282F3FFD00CDE4DD"
BuildableName = "ntfyNSE.appex"
BlueprintName = "ntfyNSE"
ReferencedContainer = "container:ntfy.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "9474F1BC282F2AA700CDE4DD"
BuildableName = "ntfy.app"
BlueprintName = "ntfy"
ReferencedContainer = "container:ntfy.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<RemoteRunnable
runnableDebuggingMode = "0"
BundleIdentifier = "io.heckel.ntfy"
RemotePath = "/var/containers/Bundle/Application/508E12DE-12D7-4A45-8D44-B351AFC4BF23/ntfy.app">
</RemoteRunnable>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "9474F1BC282F2AA700CDE4DD"
BuildableName = "ntfy.app"
BlueprintName = "ntfy"
ReferencedContainer = "container:ntfy.xcodeproj">
</BuildableReference>
</MacroExpansion>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "9474F1BC282F2AA700CDE4DD"
BuildableName = "ntfy.app"
BlueprintName = "ntfy"
ReferencedContainer = "container:ntfy.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -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
}
@ -117,7 +116,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
let baseUrl = userInfo["base_url"] as? String ?? Config.appBaseUrl
let action = message.actions?.first { $0.id == response.actionIdentifier }
// Show current topic
if message.topic != "" {
selectedBaseUrl = topicUrl(baseUrl: baseUrl, topic: message.topic)
@ -138,7 +137,20 @@ 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.
// 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))
}
}
}
}
}

View file

@ -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
@ -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 {

View file

@ -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" : {

View file

@ -4,6 +4,8 @@
<dict>
<key>AppBaseURL</key>
<string>$(APP_BASE_URL)</string>
<key>FirebaseAppDelegateProxyEnabled</key>
<false/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>

View file

@ -0,0 +1,44 @@
import CoreData
import SwiftUI
class NotificationsObservable: NSObject, ObservableObject {
private let tag = "NotificationsObservable"
private var subscriptionID: NSManagedObjectID
private lazy var fetchedResultsController: NSFetchedResultsController<Notification> = {
let fetchRequest: NSFetchRequest<Notification> = 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 {
Log.d(tag, "Fetching notifications")
try self.fetchedResultsController.performFetch()
self.notifications = self.fetchedResultsController.fetchedObjects ?? []
} catch {
Log.w(tag, "Failed to fetch notifications \(error)")
}
}
}
extension NotificationsObservable: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
DispatchQueue.main.async {
self.notifications = self.fetchedResultsController.fetchedObjects ?? []
}
}
}

View file

@ -43,7 +43,10 @@ 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()
}
@ -77,6 +80,7 @@ class Store: ObservableObject {
subscription.baseUrl = baseUrl
subscription.topic = topic
DispatchQueue.main.sync {
Log.d(Store.tag, "Storing subscription baseUrl=\(baseUrl), topic=\(topic)")
try? context.save()
}
return subscription
@ -114,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
Log.d(Store.tag, "Storing notification with ID \(notification.id ?? "<unknown>")")
try context.save()
} catch let error {
Log.w(Store.tag, "Cannot store notification (fromMessage)", error)

View file

@ -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) {
@ -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

View file

@ -0,0 +1,60 @@
import CoreData
import SwiftUI
class SubscriptionsObservable: NSObject, ObservableObject {
private let tag = "SubscriptionsObservable"
override init() {
super.init()
// This will force the initialization of notificationsFetchedResultsController
_ = self.notificationsFetchedResultsController
}
private lazy var fetchedResultsController: NSFetchedResultsController<Subscription> = {
let fetchRequest: NSFetchRequest<Subscription> = 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 {
Log.d(tag, "Fetching subscriptions")
try controller.performFetch()
} catch {
Log.w(tag, "Failed to fetch subscriptions: \(error)", error)
}
return controller
}()
private lazy var notificationsFetchedResultsController: NSFetchedResultsController<Notification> = {
let fetchRequest: NSFetchRequest<Notification> = 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 {
Log.d(tag, "Fetching notifications")
try controller.performFetch()
} catch {
Log.w(tag, "Failed to fetch notifications: \(error)", error)
}
return controller
}()
var subscriptions: [Subscription] {
fetchedResultsController.fetchedObjects ?? []
}
}
extension SubscriptionsObservable: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
Log.d(tag, "Fetching notifications")
DispatchQueue.main.async {
self.objectWillChange.send()
}
}
}

View file

@ -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

View file

@ -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<Notification>()
@ -23,6 +24,11 @@ struct NotificationListView: View {
return SubscriptionManager(store: store)
}
init(subscription: Subscription) {
self.subscription = subscription
self.notificationsModel = NotificationsObservable(subscriptionID: subscription.objectID)
}
var body: some View {
if #available(iOS 15.0, *) {
notificationList
@ -36,7 +42,7 @@ struct NotificationListView: View {
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)
}
}
@ -74,13 +80,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
@ -140,7 +146,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)

View file

@ -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<Subscription>
@ObservedObject var subscriptionsModel = SubscriptionsObservable()
@State private var showingAddDialog = false
private var subscriptionManager: SubscriptionManager {
@ -19,7 +19,7 @@ struct SubscriptionListView: View {
if #available(iOS 15.0, *) {
subscriptionList
.refreshable {
subscriptions.forEach { subscription in
subscriptionsModel.subscriptions.forEach { subscription in
subscriptionManager.poll(subscription)
}
}
@ -28,7 +28,7 @@ struct SubscriptionListView: View {
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
subscriptions.forEach { subscription in
subscriptionsModel.subscriptions.forEach { subscription in
subscriptionManager.poll(subscription)
}
} label: {
@ -43,7 +43,7 @@ struct SubscriptionListView: View {
private var subscriptionList: some View {
List {
ForEach(subscriptions) { subscription in
ForEach(subscriptionsModel.subscriptions) { subscription in
SubscriptionItemNavView(subscription: subscription)
}
}
@ -59,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)