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
2022-05-23 22:18:19 -04:00
@ EnvironmentObject private var delegate : AppDelegate
2022-05-19 21:53:04 -04:00
@ 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-17 20:55:55 -04:00
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
2022-05-17 20:55:55 -04:00
2022-05-19 22:46:41 -04:00
private var subscriptionManager : SubscriptionManager {
return SubscriptionManager ( store : store )
}
2023-10-28 03:07:56 -04:00
2022-05-16 21:00:05 -04:00
var body : some View {
2023-10-28 03:07:56 -04:00
let notificationReceived = Foundation . Notification . Name ( " notificationReceived " )
2022-05-24 20:10:32 -04:00
if #available ( iOS 15.0 , * ) {
notificationList
. refreshable {
subscriptionManager . poll ( subscription )
2023-10-28 03:07:56 -04:00
} . onReceive ( NotificationCenter . default . publisher ( for : notificationReceived ) ) { _ in
// H a n d l e t h e n o t i f i c a t i o n
subscriptionManager . poll ( subscription )
2022-05-24 20:10:32 -04:00
}
} else {
2023-10-28 03:07:56 -04:00
notificationList . onReceive ( NotificationCenter . default . publisher ( for : notificationReceived ) ) { _ in
// H a n d l e t h e n o t i f i c a t i o n
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 : {
// i O S b u g ( ? ) : W e c r e a t e a c u s t o m b a c k b u t t o n , b e c a u s e t h e o r i g i n a l b a c k b u t t o n d o e s n ' t r e s e t
// s e l e c t e d B a s e U r l e a r l y e n o u g h a n d t h e r o w s t a y s h i g h l i g h t e d f o r a l o n g t i m e ,
// w h i c h i s w e i r d a n d f e e l s w r o n g . T h i s a v o i d s t h a t b e h a v i o r .
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 ) {
2022-06-05 11:17:47 -04:00
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
2022-05-25 19:59:06 +01:00
if #available ( iOS 15.0 , * ) {
Text ( " To send notifications to this topic, simply PUT or POST to the topic URL. \n \n Example: \n `$ curl -d \" hi \" ntfy.sh/ \( subscription . topicName ( ) ) ` \n \n Detailed 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 \n Example: \n `$ curl -d \" hi \" ntfy.sh/ \( subscription . topicName ( ) ) ` \n \n Detailed instructions are available on https://ntfy.sh and https://ntfy.sh/docs. " )
. foregroundColor ( . gray )
}
2022-05-24 20:10:32 -04:00
}
. padding ( 40 )
}
} )
2022-05-24 20:46:23 -04:00
. 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 > ( )
} ) {
2022-05-17 20:55:55 -04:00
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 ( ) {
2022-05-20 14:07:53 -04:00
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 ) ) )
2022-05-20 15:43:55 -04:00
DispatchQueue . global ( qos : . background ) . async {
2022-06-03 22:49:04 -04:00
let user = store . getUser ( baseUrl : subscription . baseUrl ! ) ? . toBasicUser ( )
2022-05-20 15:43:55 -04:00
ApiService . shared . publish (
subscription : subscription ,
2022-06-03 22:49:04 -04:00
user : user ,
2022-05-20 15:43:55 -04:00
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 ( ) {
2022-05-20 15:43:55 -04:00
DispatchQueue . global ( qos : . background ) . async {
subscriptionManager . unsubscribe ( subscription )
}
2022-05-23 22:18:19 -04:00
delegate . selectedBaseUrl = nil
2022-05-20 09:18:57 -04:00
}
private func deleteAll ( ) {
2022-05-20 15:43:55 -04:00
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 ( ) {
2022-05-20 15:43:55 -04:00
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
}
2022-05-24 20:46:23 -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
2022-05-24 20:46:23 -04:00
}
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
}
2022-05-20 14:07:53 -04:00
struct NotificationRowView : View {
2022-05-20 15:43:55 -04:00
@ EnvironmentObject private var store : Store
@ ObservedObject var notification : Notification
2022-05-24 20:10:32 -04:00
2022-05-20 14:07:53 -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: T h i s g i v e s n o f e e d b a c k t o t h e u s e r , a n d i t o n l y w o r k s i f t h e t e x t i s t a p p e d
UIPasteboard . general . setValue ( notification . formatMessage ( ) , forPasteboardType : UTType . plainText . identifier )
}
2022-05-20 14:07:53 -04:00
}
}
struct NotificationListView_Previews : PreviewProvider {
static var previews : some View {
let store = Store . preview
2022-05-20 15:43:55 -04:00
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 " ] ! )
2022-05-20 15:43:55 -04:00
NotificationListView ( subscription : subscriptionWithNotifications )
. environment ( \ . managedObjectContext , store . context )
. environmentObject ( store )
NotificationListView ( subscription : subscriptionWithoutNotifications )
. environment ( \ . managedObjectContext , store . context )
. environmentObject ( store )
}
2022-05-20 14:07:53 -04:00
}
}
2022-05-27 23:49:13 -04:00