More previews, more cleanup; add alert for unsubscribing

This commit is contained in:
Philipp Heckel 2022-05-20 14:07:53 -04:00
parent af87a440cd
commit 364f1a6381
8 changed files with 165 additions and 158 deletions

View file

@ -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 = "<group>"; };
9474F1E8282F3FFD00CDE4DD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9474F1F12830825600CDE4DD /* SubscriptionsListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionsListView.swift; sourceTree = "<group>"; };
9474F1F12830825600CDE4DD /* SubscriptionListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionListView.swift; sourceTree = "<group>"; };
9474F1F62830830700CDE4DD /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = "<group>"; };
9474F1F82830835400CDE4DD /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
9474F1FA28308A2B00CDE4DD /* SubscriptionRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionRowView.swift; sourceTree = "<group>"; };
9474F1FC2831311A00CDE4DD /* SubscriptionAddView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionAddView.swift; sourceTree = "<group>"; };
9474F1FE28316ACE00CDE4DD /* Subscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = "<group>"; };
9474F2042831CDBF00CDE4DD /* ntfyNSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ntfyNSE.entitlements; sourceTree = "<group>"; };
9474F20728331F3900CDE4DD /* NotificationListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationListView.swift; sourceTree = "<group>"; };
9474F20828331F3A00CDE4DD /* NotificationRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationRowView.swift; sourceTree = "<group>"; };
9474F20B283321C300CDE4DD /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = "<group>"; };
9474F20E283326C500CDE4DD /* ApiService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiService.swift; sourceTree = "<group>"; };
9474F211283327C200CDE4DD /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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 */,

View file

@ -10,7 +10,7 @@ class Store: ObservableObject {
var context: NSManagedObjectContext {
return container.viewContext
}
private var subscriptions: Set<AnyCancellable> = []
private var cancellables: Set<AnyCancellable> = []
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

View file

@ -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<String> = ["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)
}
}

View file

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

View file

@ -1,5 +1,4 @@
import SwiftUI
import FirebaseMessaging
struct SubscriptionAddView: View {
private let tag = "SubscriptionAddView"

View file

@ -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<Subscription>
@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)
}
}

View file

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

View file

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