oncall-mobile-ios/ntfy/Views/NotificationListView.swift

352 lines
14 KiB
Swift
Raw Normal View History

2022-05-16 21:00:05 -04:00
import SwiftUI
2022-05-25 20:06:59 -04:00
import UniformTypeIdentifiers
2022-05-16 21:00:05 -04:00
enum ActiveAlert {
case clear, unsubscribe, selected
}
struct NotificationListView: View {
2022-05-19 22:05:32 -04:00
private let tag = "NotificationListView"
2022-05-20 08:58:22 -04:00
@EnvironmentObject private var delegate: AppDelegate
@EnvironmentObject private var store: Store
2022-05-20 08:58:22 -04:00
2022-05-16 21:00:05 -04:00
@ObservedObject var subscription: Subscription
2022-05-16 21:00:05 -04:00
@State private var editMode = EditMode.inactive
@State private var selection = Set<Notification>()
2022-05-20 08:58:22 -04:00
2022-05-16 21:00:05 -04:00
@State private var showAlert = false
@State private var activeAlert: ActiveAlert = .clear
private var subscriptionManager: SubscriptionManager {
return SubscriptionManager(store: store)
}
2022-05-16 21:00:05 -04:00
var body: some View {
let notificationReceived = Foundation.Notification.Name("notificationReceived")
2022-05-24 20:10:32 -04:00
if #available(iOS 15.0, *) {
notificationList
.refreshable {
subscriptionManager.poll(subscription)
}.onReceive(NotificationCenter.default.publisher(for: notificationReceived)) { _ in
// Handle the notification
subscriptionManager.poll(subscription)
2022-05-24 20:10:32 -04:00
}
} else {
notificationList.onReceive(NotificationCenter.default.publisher(for: notificationReceived)) { _ in
// Handle the notification
subscriptionManager.poll(subscription)
}
2022-05-24 20:10:32 -04:00
}
}
private var notificationList: some View {
List(selection: $selection) {
ForEach(subscription.notificationsSorted(), id: \.self) { notification in
NotificationRowView(notification: notification)
}
}
.listStyle(PlainListStyle())
.navigationBarTitleDisplayMode(.inline)
.environment(\.editMode, self.$editMode)
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
if (self.editMode != .active) {
Button(action: {
// iOS bug (?): We create a custom back button, because the original back button doesn't reset
// selectedBaseUrl early enough and the row stays highlighted for a long time,
// which is weird and feels wrong. This avoids that behavior.
self.delegate.selectedBaseUrl = nil
}){
Image(systemName: "chevron.left")
}
2022-05-27 23:49:13 -04:00
.padding([.top, .bottom, .trailing], 40)
2022-05-24 20:10:32 -04:00
}
}
ToolbarItem(placement: .principal) {
Text(subscription.topicName())
2022-05-28 21:27:16 -04:00
.font(.headline)
.lineLimit(1)
2022-05-24 20:10:32 -04:00
}
ToolbarItem(placement: .navigationBarTrailing) {
if (self.editMode == .active) {
editButton
} else {
Menu {
if #unavailable(iOS 15.0) {
Button("Refresh") {
subscriptionManager.poll(subscription)
}
}
if subscription.notificationCount() > 0 {
editButton
}
Button("Send test notification") {
self.sendTestNotification()
}
if subscription.notificationCount() > 0 {
Button("Clear all notifications") {
self.showAlert = true
self.activeAlert = .clear
}
}
Button("Unsubscribe") {
self.showAlert = true
self.activeAlert = .unsubscribe
}
} label: {
Image(systemName: "ellipsis.circle")
2022-05-27 23:49:13 -04:00
.padding([.leading], 40)
2022-05-24 20:10:32 -04:00
}
}
}
ToolbarItem(placement: .navigationBarLeading) {
if (self.editMode == .active) {
Button(action: {
self.showAlert = true
self.activeAlert = .selected
}) {
Text("Delete")
.foregroundColor(.red)
}
}
}
}
.alert(isPresented: $showAlert) {
switch activeAlert {
case .clear:
return Alert(
title: Text("Clear notifications"),
message: Text("Do you really want to delete all of the notifications in this topic?"),
primaryButton: .destructive(
Text("Permanently delete"),
action: deleteAll
),
secondaryButton: .cancel())
case .unsubscribe:
return 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: unsubscribe
),
secondaryButton: .cancel())
case .selected:
return Alert(
title: Text("Delete"),
message: Text("Do you really want to delete these selected notifications?"),
primaryButton: .destructive(
Text("Delete"),
action: deleteSelected
),
secondaryButton: .cancel())
}
}
.overlay(Group {
if subscription.notificationCount() == 0 {
VStack {
Text("You haven't received any notifications for this topic yet.")
.font(.title2)
.foregroundColor(.gray)
.multilineTextAlignment(.center)
.padding(.bottom)
2022-05-27 23:49:13 -04:00
if #available(iOS 15.0, *) {
Text("To send notifications to this topic, simply PUT or POST to the topic URL.\n\nExample:\n`$ curl -d \"hi\" ntfy.sh/\(subscription.topicName())`\n\nDetailed instructions are available on [ntfy.sh](https://ntfy.sh) and [in the docs](https://ntfy.sh/docs).")
.foregroundColor(.gray)
} else {
Text("To send notifications to this topic, simply PUT or POST to the topic URL.\n\nExample:\n`$ curl -d \"hi\" ntfy.sh/\(subscription.topicName())`\n\nDetailed instructions are available on https://ntfy.sh and https://ntfy.sh/docs.")
.foregroundColor(.gray)
}
2022-05-24 20:10:32 -04:00
}
.padding(40)
}
})
.onAppear {
cancelSubscriptionNotifications()
}
2022-05-16 21:00:05 -04:00
}
2022-05-20 08:58:22 -04:00
2022-05-16 21:00:05 -04:00
private var editButton: some View {
if editMode == .inactive {
return Button(action: {
self.editMode = .active
self.selection = Set<Notification>()
}) {
Text("Select messages")
2022-05-16 21:00:05 -04:00
}
} else {
return Button(action: {
self.editMode = .inactive
self.selection = Set<Notification>()
}) {
Text("Done")
}
}
}
2022-05-20 08:58:22 -04:00
2022-05-20 09:18:57 -04:00
private func sendTestNotification() {
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"]
2022-05-20 09:18:57 -04:00
let priority = Int.random(in: 1..<6)
let tags = Array(possibleTags.shuffled().prefix(Int.random(in: 0..<4)))
DispatchQueue.global(qos: .background).async {
2022-06-03 22:49:04 -04:00
let user = store.getUser(baseUrl: subscription.baseUrl!)?.toBasicUser()
ApiService.shared.publish(
subscription: subscription,
2022-06-03 22:49:04 -04:00
user: user,
message: "This is a test notification from the ntfy iOS app. It has a priority of \(priority). If you send another one, it may look different.",
title: "Test: You can set a title if you like",
priority: priority,
tags: tags
)
}
2022-05-20 09:18:57 -04:00
}
private func unsubscribe() {
DispatchQueue.global(qos: .background).async {
subscriptionManager.unsubscribe(subscription)
}
delegate.selectedBaseUrl = nil
2022-05-20 09:18:57 -04:00
}
private func deleteAll() {
DispatchQueue.global(qos: .background).async {
store.delete(allNotificationsFor: subscription)
}
2022-05-20 09:18:57 -04:00
}
2022-05-20 08:58:22 -04:00
private func deleteSelected() {
DispatchQueue.global(qos: .background).async {
store.delete(notifications: selection)
selection = Set<Notification>()
}
2022-05-20 08:58:22 -04:00
editMode = .inactive
2022-05-16 21:00:05 -04:00
}
private func cancelSubscriptionNotifications() {
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.getDeliveredNotifications { notifications in
let ids = notifications
.filter { notification in
2022-05-28 21:27:16 -04:00
let userInfo = notification.request.content.userInfo
if let baseUrl = userInfo["base_url"] as? String, let topic = userInfo["topic"] as? String {
return baseUrl == subscription.baseUrl && topic == subscription.topic
}
return false
}
.map { notification in
notification.request.identifier
}
if !ids.isEmpty {
Log.d(tag, "Cancelling \(ids.count) notification(s) from notification center")
notificationCenter.removeDeliveredNotifications(withIdentifiers: ids)
}
}
}
2022-05-16 21:00:05 -04:00
}
struct NotificationRowView: View {
@EnvironmentObject private var store: Store
@ObservedObject var notification: Notification
2022-05-24 20:10:32 -04:00
var body: some View {
2022-05-24 20:10:32 -04:00
if #available(iOS 15.0, *) {
notificationRow
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
store.delete(notification: notification)
} label: {
Label("Delete", systemImage: "trash.circle")
}
}
} else {
notificationRow
}
}
private var notificationRow: some View {
VStack(alignment: .leading, spacing: 0) {
2022-05-24 22:27:04 -04:00
HStack(alignment: .center, spacing: 2) {
Text(notification.shortDateTime())
.font(.subheadline)
.foregroundColor(.gray)
if [1,2,4,5].contains(notification.priority) {
Image("priority-\(notification.priority)")
.resizable()
.scaledToFit()
.frame(width: 16, height: 16)
}
}
2022-05-27 23:49:13 -04:00
.padding([.bottom], 2)
2022-05-25 11:26:23 -04:00
if let title = notification.formatTitle(), title != "" {
2022-05-24 20:10:32 -04:00
Text(title)
.font(.headline)
.bold()
2022-05-27 23:49:13 -04:00
.padding([.bottom], 2)
2022-05-24 20:10:32 -04:00
}
2022-05-25 11:26:23 -04:00
Text(notification.formatMessage())
2022-05-24 20:10:32 -04:00
.font(.body)
2022-05-25 11:26:23 -04:00
if !notification.nonEmojiTags().isEmpty {
Text("Tags: " + notification.nonEmojiTags().joined(separator: ", "))
.font(.subheadline)
.foregroundColor(.gray)
2022-05-27 23:49:13 -04:00
.padding([.top], 2)
}
if !notification.actionsList().isEmpty {
HStack {
ForEach(notification.actionsList()) { action in
if #available(iOS 15, *) {
Button(action.label) {
ActionExecutor.execute(action)
}
2022-05-28 21:27:16 -04:00
.buttonStyle(.borderedProminent)
2022-05-27 23:49:13 -04:00
} else {
2022-05-28 21:27:16 -04:00
Button(action: {
2022-05-27 23:49:13 -04:00
ActionExecutor.execute(action)
2022-05-28 21:27:16 -04:00
}) {
Text(action.label)
.padding(EdgeInsets(top: 10.0, leading: 10.0, bottom: 10.0, trailing: 10.0))
.foregroundColor(.white)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.white, lineWidth: 2)
)
2022-05-27 23:49:13 -04:00
}
2022-05-28 21:27:16 -04:00
.background(Color.accentColor)
.cornerRadius(10)
2022-05-27 23:49:13 -04:00
}
}
}
.padding([.top], 5)
2022-05-25 11:26:23 -04:00
}
2022-05-24 20:10:32 -04:00
}
.padding(.all, 4)
2022-05-25 20:06:59 -04:00
.onTapGesture {
// TODO: This gives no feedback to the user, and it only works if the text is tapped
UIPasteboard.general.setValue(notification.formatMessage(), forPasteboardType: UTType.plainText.identifier)
}
}
}
struct NotificationListView_Previews: PreviewProvider {
static var previews: some View {
let store = Store.preview
Group {
2022-06-03 22:49:04 -04:00
let subscriptionWithNotifications = store.makeSubscription(store.context, "stats", Store.sampleMessages["stats"]!)
let subscriptionWithoutNotifications = store.makeSubscription(store.context, "announcements", Store.sampleMessages["announcements"]!)
NotificationListView(subscription: subscriptionWithNotifications)
.environment(\.managedObjectContext, store.context)
.environmentObject(store)
NotificationListView(subscription: subscriptionWithoutNotifications)
.environment(\.managedObjectContext, store.context)
.environmentObject(store)
}
}
}
2022-05-27 23:49:13 -04:00