From 364f1a638140cad26704c095fd1d7b3619e5374b Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Fri, 20 May 2022 14:07:53 -0400 Subject: [PATCH] More previews, more cleanup; add alert for unsubscribing --- ntfy.xcodeproj/project.pbxproj | 16 +--- ntfy/Persistence/Store.swift | 21 ++--- ntfy/Views/NotificationListView.swift | 33 ++++++- ntfy/Views/NotificationRowView.swift | 25 ------ ntfy/Views/SubscriptionAddView.swift | 1 - ntfy/Views/SubscriptionListView.swift | 119 +++++++++++++++++++++++++ ntfy/Views/SubscriptionRowView.swift | 36 -------- ntfy/Views/SubscriptionsListView.swift | 72 --------------- 8 files changed, 165 insertions(+), 158 deletions(-) delete mode 100644 ntfy/Views/NotificationRowView.swift create mode 100644 ntfy/Views/SubscriptionListView.swift delete mode 100644 ntfy/Views/SubscriptionRowView.swift delete mode 100644 ntfy/Views/SubscriptionsListView.swift diff --git a/ntfy.xcodeproj/project.pbxproj b/ntfy.xcodeproj/project.pbxproj index d2cd723..994f0b4 100644 --- a/ntfy.xcodeproj/project.pbxproj +++ b/ntfy.xcodeproj/project.pbxproj @@ -16,16 +16,14 @@ 9474F1DC282F30B500CDE4DD /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9474F1DB282F30B500CDE4DD /* GoogleService-Info.plist */; }; 9474F1E7282F3FFD00CDE4DD /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F1E6282F3FFD00CDE4DD /* NotificationService.swift */; }; 9474F1EB282F3FFD00CDE4DD /* ntfyNSE.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 9474F1E4282F3FFD00CDE4DD /* ntfyNSE.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 9474F1F22830825600CDE4DD /* SubscriptionsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F1F12830825600CDE4DD /* SubscriptionsListView.swift */; }; + 9474F1F22830825600CDE4DD /* SubscriptionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F1F12830825600CDE4DD /* SubscriptionListView.swift */; }; 9474F1F72830830700CDE4DD /* ntfy.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 9474F1F52830830700CDE4DD /* ntfy.xcdatamodeld */; }; 9474F1F92830835400CDE4DD /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F1F82830835400CDE4DD /* Store.swift */; }; - 9474F1FB28308A2B00CDE4DD /* SubscriptionRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F1FA28308A2B00CDE4DD /* SubscriptionRowView.swift */; }; 9474F1FD2831311A00CDE4DD /* SubscriptionAddView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F1FC2831311A00CDE4DD /* SubscriptionAddView.swift */; }; 9474F1FF28316ACE00CDE4DD /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F1FE28316ACE00CDE4DD /* Subscription.swift */; }; 9474F2052831D51500CDE4DD /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F1F82830835400CDE4DD /* Store.swift */; }; 9474F2062831D73C00CDE4DD /* ntfy.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 9474F1F52830830700CDE4DD /* ntfy.xcdatamodeld */; }; 9474F20928331F3A00CDE4DD /* NotificationListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F20728331F3900CDE4DD /* NotificationListView.swift */; }; - 9474F20A28331F3A00CDE4DD /* NotificationRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F20828331F3A00CDE4DD /* NotificationRowView.swift */; }; 9474F20C283321C300CDE4DD /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F20B283321C300CDE4DD /* Notification.swift */; }; 9474F20F283326C500CDE4DD /* ApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F20E283326C500CDE4DD /* ApiService.swift */; }; 9474F212283327C200CDE4DD /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F211283327C200CDE4DD /* Helpers.swift */; }; @@ -74,15 +72,13 @@ 9474F1E4282F3FFD00CDE4DD /* ntfyNSE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ntfyNSE.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 9474F1E6282F3FFD00CDE4DD /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 9474F1E8282F3FFD00CDE4DD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 9474F1F12830825600CDE4DD /* SubscriptionsListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionsListView.swift; sourceTree = ""; }; + 9474F1F12830825600CDE4DD /* SubscriptionListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionListView.swift; sourceTree = ""; }; 9474F1F62830830700CDE4DD /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; 9474F1F82830835400CDE4DD /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; - 9474F1FA28308A2B00CDE4DD /* SubscriptionRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionRowView.swift; sourceTree = ""; }; 9474F1FC2831311A00CDE4DD /* SubscriptionAddView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionAddView.swift; sourceTree = ""; }; 9474F1FE28316ACE00CDE4DD /* Subscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = ""; }; 9474F2042831CDBF00CDE4DD /* ntfyNSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ntfyNSE.entitlements; sourceTree = ""; }; 9474F20728331F3900CDE4DD /* NotificationListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationListView.swift; sourceTree = ""; }; - 9474F20828331F3A00CDE4DD /* NotificationRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationRowView.swift; sourceTree = ""; }; 9474F20B283321C300CDE4DD /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; 9474F20E283326C500CDE4DD /* ApiService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiService.swift; sourceTree = ""; }; 9474F211283327C200CDE4DD /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; @@ -175,10 +171,8 @@ children = ( 9474F1C2282F2AA700CDE4DD /* ContentView.swift */, 9474F1FC2831311A00CDE4DD /* SubscriptionAddView.swift */, - 9474F20828331F3A00CDE4DD /* NotificationRowView.swift */, 9474F20728331F3900CDE4DD /* NotificationListView.swift */, - 9474F1FA28308A2B00CDE4DD /* SubscriptionRowView.swift */, - 9474F1F12830825600CDE4DD /* SubscriptionsListView.swift */, + 9474F1F12830825600CDE4DD /* SubscriptionListView.swift */, ); path = Views; sourceTree = ""; @@ -325,12 +319,10 @@ 9474F217283531A300CDE4DD /* Log.swift in Sources */, 9474F20928331F3A00CDE4DD /* NotificationListView.swift in Sources */, 94A3F7C8283734D900C48E79 /* SubscriptionManager.swift in Sources */, - 9474F20A28331F3A00CDE4DD /* NotificationRowView.swift in Sources */, 9474F1D2282F2D2C00CDE4DD /* AppDelegate.swift in Sources */, 9474F1C3282F2AA700CDE4DD /* ContentView.swift in Sources */, - 9474F1FB28308A2B00CDE4DD /* SubscriptionRowView.swift in Sources */, 9474F20C283321C300CDE4DD /* Notification.swift in Sources */, - 9474F1F22830825600CDE4DD /* SubscriptionsListView.swift in Sources */, + 9474F1F22830825600CDE4DD /* SubscriptionListView.swift in Sources */, 9474F1FD2831311A00CDE4DD /* SubscriptionAddView.swift in Sources */, 9474F1FF28316ACE00CDE4DD /* Subscription.swift in Sources */, 9474F1C1282F2AA700CDE4DD /* AppMain.swift in Sources */, diff --git a/ntfy/Persistence/Store.swift b/ntfy/Persistence/Store.swift index 63d01e2..c1e3262 100644 --- a/ntfy/Persistence/Store.swift +++ b/ntfy/Persistence/Store.swift @@ -10,7 +10,7 @@ class Store: ObservableObject { var context: NSManagedObjectContext { return container.viewContext } - private var subscriptions: Set = [] + private var cancellables: Set = [] init(inMemory: Bool = false) { let storeUrl = (inMemory) ? URL(fileURLWithPath: "/dev/null") : FileManager.default @@ -49,8 +49,7 @@ class Store: ObservableObject { self.container.viewContext.refreshAllObjects() } } - .store(in: &subscriptions) - + .store(in: &cancellables) } func saveSubscription(baseUrl: String, topic: String) { @@ -158,10 +157,12 @@ class Store: ObservableObject { extension Store { - private static let topics = [ + static let sampleData = [ "stats": [ - Message(id: "1", time: 1653048956, message: "200 users/h\n123 IPs", title: nil), - Message(id: "2", time: 1653058956, message: "201 users/h\n80 IPs", title: nil) + Message(id: "1", time: 1653048956, message: "In the last 24 hours, hyou had 5,000 users across 13 countries visit your website", title: "Record visitor numbers"), + Message(id: "2", time: 1653058956, message: "201 users/h\n80 IPs", title: "This is a title"), + Message(id: "3", time: 1643058956, message: "This message does not have a title, but is instead super long. Like really really long. It can't be any longer I think. I mean, there is s 4,000 byte limit of the message, so I guess I have to make this 4,000 bytes long. Or do I? 😁 I don't know. It's quite tedious to come up with something so long, so I'll stop now. Bye!", title: nil) + ], "backups": [], "announcements": [], @@ -172,16 +173,16 @@ extension Store { static var preview: Store = { let store = Store(inMemory: true) store.context.perform { - topics.forEach { topic, messages in - let notifications = messages.map { store.makeNotification(store.context, $0) } - store.makeSubscription(store.context, topic, notifications) + sampleData.forEach { topic, messages in + store.makeSubscription(store.context, topic, messages) } } return store }() @discardableResult - func makeSubscription(_ context: NSManagedObjectContext, _ topic: String, _ notifications: [Notification]) -> Subscription { + func makeSubscription(_ context: NSManagedObjectContext, _ topic: String, _ messages: [Message]) -> Subscription { + let notifications = messages.map { makeNotification(context, $0) } let subscription = Subscription(context: context) subscription.baseUrl = appBaseUrl subscription.topic = topic diff --git a/ntfy/Views/NotificationListView.swift b/ntfy/Views/NotificationListView.swift index 9b51159..a5412b2 100644 --- a/ntfy/Views/NotificationListView.swift +++ b/ntfy/Views/NotificationListView.swift @@ -7,7 +7,6 @@ enum ActiveAlert { struct NotificationListView: View { private let tag = "NotificationListView" - @Environment(\.managedObjectContext) var context @Environment(\.presentationMode) var presentationMode @EnvironmentObject private var store: Store @@ -133,7 +132,7 @@ struct NotificationListView: View { } private func sendTestNotification() { - let possibleTags = ["warning", "skull", "success", "triangular_flag_on_post", "de", "us", "dog", "cat", "rotating_light", "bike", "backup", "rsync", "this-s-a-tag", "ios"] + let possibleTags: Array = ["warning", "skull", "success", "triangular_flag_on_post", "de", "us", "dog", "cat", "rotating_light", "bike", "backup", "rsync", "this-s-a-tag", "ios"] let priority = Int.random(in: 1..<6) let tags = Array(possibleTags.shuffled().prefix(Int.random(in: 0..<4))) ApiService.shared.publish( @@ -177,3 +176,33 @@ struct NotificationListView: View { } } } + +struct NotificationRowView: View { + let notification: Notification + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + Text(notification.shortDateTime()) + .font(.subheadline) + .foregroundColor(.gray) + if let title = notification.title, title != "" { + Text(title) + .font(.headline) + .bold() + } + Text(notification.message ?? "") + .font(.body) + } + .padding(.all, 4) + } +} + +struct NotificationListView_Previews: PreviewProvider { + static var previews: some View { + let store = Store.preview + let subscription = store.makeSubscription(store.context, "stats", Store.sampleData["stats"]!) + NotificationListView(subscription: subscription) + .environment(\.managedObjectContext, store.context) + .environmentObject(store) + } +} diff --git a/ntfy/Views/NotificationRowView.swift b/ntfy/Views/NotificationRowView.swift deleted file mode 100644 index 2ffd240..0000000 --- a/ntfy/Views/NotificationRowView.swift +++ /dev/null @@ -1,25 +0,0 @@ -import SwiftUI - -struct NotificationRowView: View { - let notification: Notification - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - HStack { - Text(notification.title ?? "") - .font(.headline) - .bold() - .lineLimit(1) - Spacer() - Text(notification.shortDateTime()) - .font(.subheadline) - .foregroundColor(.gray) - - } - Spacer() - Text(notification.message ?? "") - .font(.body) - } - .padding(.all, 4) - } -} diff --git a/ntfy/Views/SubscriptionAddView.swift b/ntfy/Views/SubscriptionAddView.swift index fa57c5e..110069e 100644 --- a/ntfy/Views/SubscriptionAddView.swift +++ b/ntfy/Views/SubscriptionAddView.swift @@ -1,5 +1,4 @@ import SwiftUI -import FirebaseMessaging struct SubscriptionAddView: View { private let tag = "SubscriptionAddView" diff --git a/ntfy/Views/SubscriptionListView.swift b/ntfy/Views/SubscriptionListView.swift new file mode 100644 index 0000000..24c6a3e --- /dev/null +++ b/ntfy/Views/SubscriptionListView.swift @@ -0,0 +1,119 @@ +// https://www.raywenderlich.com/14958063-modern-efficient-core-data +// https://www.hackingwithswift.com/books/ios-swiftui/how-to-combine-core-data-and-swiftui + +import SwiftUI +import CoreData +import FirebaseMessaging +import UserNotifications + +struct SubscriptionListView: View { + let tag = "SubscriptionList" + + @EnvironmentObject private var store: Store + @FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Subscription.topic, ascending: true)]) var subscriptions: FetchedResults + + @State private var unsubscribeAlert = false + @State private var unsubscribeSubscription: Subscription? + + private var subscriptionManager: SubscriptionManager { + return SubscriptionManager(store: store) + } + + var body: some View { + NavigationView { + List { + ForEach(subscriptions) { subscription in + ZStack { + NavigationLink(destination: NotificationListView(subscription: subscription)) { + EmptyView() + } + .opacity(0.0) + .buttonStyle(PlainButtonStyle()) + + SubscriptionRowView(subscription: subscription) + } + .swipeActions(edge: .trailing) { + Button(role: .destructive) { + unsubscribeAlert = true + unsubscribeSubscription = subscription + //subscriptionManager.unsubscribe(subscription) + } label: { + Label("Delete", systemImage: "trash.circle") + } + } + .alert(isPresented: $unsubscribeAlert) { + Alert( + title: Text("Unsubscribe"), + message: Text("Do you really want to unsubscribe from this topic and delete all of the notifications you received?"), + primaryButton: .destructive( + Text("Unsubscribe"), + action: { + subscriptionManager.unsubscribe(unsubscribeSubscription!) + unsubscribeAlert = false + unsubscribeSubscription = nil + } + ), + secondaryButton: .cancel() + ) + } + } + } + .listStyle(PlainListStyle()) + .navigationTitle("Subscribed topics") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + NavigationLink( + destination: SubscriptionAddView() + ) { + Image(systemName: "plus") + } + } + } + .overlay(Group { + if subscriptions.isEmpty { + Text("No topics") + .font(.headline) + .foregroundColor(.secondary) + } + }) + } + .navigationViewStyle(StackNavigationViewStyle()) + } + +} + +struct SubscriptionRowView: View { + @ObservedObject var subscription: Subscription + + var body: some View { + let totalNotificationCount = subscription.notificationCount() + VStack(alignment: .leading, spacing: 0) { + HStack { + Text(subscription.displayName()) + .font(.headline) + .bold() + .lineLimit(1) + Spacer() + Text(subscription.lastNotification()?.shortDateTime() ?? "") + .font(.subheadline) + .foregroundColor(.gray) + Image(systemName: "chevron.forward") + .font(.system(size: 12.0)) + .foregroundColor(.gray) + } + Spacer() + Text("\(totalNotificationCount) notification\(totalNotificationCount != 1 ? "s" : "")") + .font(.subheadline) + .foregroundColor(.gray) + } + .padding(.all, 4) + } +} + +struct SubscriptionsListView_Previews: PreviewProvider { + static var previews: some View { + SubscriptionListView() + .environment(\.managedObjectContext, Store.preview.context) + .environmentObject(Store.preview) + } +} diff --git a/ntfy/Views/SubscriptionRowView.swift b/ntfy/Views/SubscriptionRowView.swift deleted file mode 100644 index 83dd067..0000000 --- a/ntfy/Views/SubscriptionRowView.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// SubscriptionRow.swift -// ntfy.sh -// -// Created by Andrew Cope on 1/15/22. -// - -import SwiftUI - -struct SubscriptionRowView: View { - @ObservedObject var subscription: Subscription - - var body: some View { - let totalNotificationCount = subscription.notificationCount() - VStack(alignment: .leading, spacing: 0) { - HStack { - Text(subscription.displayName()) - .font(.headline) - .bold() - .lineLimit(1) - Spacer() - Text(subscription.lastNotification()?.shortDateTime() ?? "") - .font(.subheadline) - .foregroundColor(.gray) - Image(systemName: "chevron.forward") - .font(.system(size: 12.0)) - .foregroundColor(.gray) - } - Spacer() - Text("\(totalNotificationCount) notification\(totalNotificationCount != 1 ? "s" : "")") - .font(.subheadline) - .foregroundColor(.gray) - } - .padding(.all, 4) - } -} diff --git a/ntfy/Views/SubscriptionsListView.swift b/ntfy/Views/SubscriptionsListView.swift deleted file mode 100644 index 70e710e..0000000 --- a/ntfy/Views/SubscriptionsListView.swift +++ /dev/null @@ -1,72 +0,0 @@ -// https://www.raywenderlich.com/14958063-modern-efficient-core-data -// https://www.hackingwithswift.com/books/ios-swiftui/how-to-combine-core-data-and-swiftui - -import SwiftUI -import CoreData -import FirebaseMessaging -import UserNotifications - -struct SubscriptionListView: View { - let tag = "SubscriptionList" - - @Environment(\.managedObjectContext) var context - @EnvironmentObject private var store: Store - @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "topic", ascending: true)]) var subscriptions: FetchedResults - - private var subscriptionManager: SubscriptionManager { - return SubscriptionManager(store: store) - } - - var body: some View { - NavigationView { - List { - ForEach(subscriptions) { subscription in - ZStack { - NavigationLink(destination: NotificationListView(subscription: subscription)) { - EmptyView() - } - .opacity(0.0) - .buttonStyle(PlainButtonStyle()) - - SubscriptionRowView(subscription: subscription) - } - .swipeActions(edge: .trailing) { - Button(role: .destructive) { - subscriptionManager.unsubscribe(subscription) - } label: { - Label("Delete", systemImage: "trash.circle") - } - } - } - } - .listStyle(PlainListStyle()) - .navigationTitle("Subscribed topics") - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - NavigationLink( - destination: SubscriptionAddView() - ) { - Image(systemName: "plus") - } - } - } - .overlay(Group { - if subscriptions.isEmpty { - Text("No topics") - .font(.headline) - .foregroundColor(.secondary) - } - }) - } - .navigationViewStyle(StackNavigationViewStyle()) - } -} - - -struct SubscriptionsList_Previews: PreviewProvider { - static var previews: some View { - SubscriptionListView() - .environment(\.managedObjectContext, Store.preview.context) - .environmentObject(Store.preview) - } -}