diff --git a/app/src/main/java/net/underdesk/circolapp/viewmodels/RemindersViewModel.kt b/app/src/main/java/net/underdesk/circolapp/viewmodels/RemindersViewModel.kt index fcdade2..513b591 100644 --- a/app/src/main/java/net/underdesk/circolapp/viewmodels/RemindersViewModel.kt +++ b/app/src/main/java/net/underdesk/circolapp/viewmodels/RemindersViewModel.kt @@ -43,7 +43,7 @@ class RemindersViewModel internal constructor( val circulars: LiveData> = Transformations.switchMap(DoubleTrigger(query, schoolID)) { input -> if (input.first == null || input.first == "") { - circularRepository.circularDao.getReminders(input.second ?: 0).asLiveData() + circularRepository.circularDao.getFlowReminders(input.second ?: 0).asLiveData() } else { circularRepository.circularDao.searchReminders( "%${input.first}%", diff --git a/ios/circolapp/circolapp.xcodeproj/project.pbxproj b/ios/circolapp/circolapp.xcodeproj/project.pbxproj index 922bd56..ca67e18 100644 --- a/ios/circolapp/circolapp.xcodeproj/project.pbxproj +++ b/ios/circolapp/circolapp.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 9512D3C1257AB4F60023C3A1 /* NewReminderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9512D3C0257AB4F60023C3A1 /* NewReminderView.swift */; }; 952DEDDF2576F8DC001DF85D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 952DEDDE2576F8DC001DF85D /* SceneDelegate.swift */; }; 9547205B2573B688005AA401 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 9547205A2573B688005AA401 /* Settings.bundle */; }; 954E68352574E3890034EBA8 /* iOSServerApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 954E68342574E3890034EBA8 /* iOSServerApi.swift */; }; @@ -41,6 +42,7 @@ /* Begin PBXFileReference section */ 892B265487980F6A344AC2A7 /* Pods-circolapp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-circolapp.debug.xcconfig"; path = "Target Support Files/Pods-circolapp/Pods-circolapp.debug.xcconfig"; sourceTree = ""; }; 8BEA78E7C5BBEF0119834B33 /* Pods_circolapp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_circolapp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9512D3C0257AB4F60023C3A1 /* NewReminderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewReminderView.swift; sourceTree = ""; }; 952C5954255C57650018C010 /* shared.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = shared.framework; path = "../../shared/build/xcode-frameworks/shared.framework"; sourceTree = ""; }; 952DEDDE2576F8DC001DF85D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 9547205A2573B688005AA401 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; @@ -97,6 +99,7 @@ 95CA31B1255C1EDE00AC095B /* ContentView.swift */, 95CA31BF255C28C300AC095B /* CircularView.swift */, 95BC3BF72572BCF900F24400 /* AttachmentView.swift */, + 9512D3C0257AB4F60023C3A1 /* NewReminderView.swift */, ); path = View; sourceTree = ""; @@ -278,6 +281,7 @@ 954E68352574E3890034EBA8 /* iOSServerApi.swift in Sources */, 95CA31B0255C1EDE00AC095B /* AppDelegate.swift in Sources */, 95BC3BF82572BCF900F24400 /* AttachmentView.swift in Sources */, + 9512D3C1257AB4F60023C3A1 /* NewReminderView.swift in Sources */, 954E683D2574ED9E0034EBA8 /* UserDefaultsExtensions.swift in Sources */, 95C46A51255D3A34007A75E5 /* CircularViewModel.swift in Sources */, ); diff --git a/ios/circolapp/circolapp/AppDelegate.swift b/ios/circolapp/circolapp/AppDelegate.swift index 8d76015..87ee286 100644 --- a/ios/circolapp/circolapp/AppDelegate.swift +++ b/ios/circolapp/circolapp/AppDelegate.swift @@ -24,26 +24,26 @@ import Firebase class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { FirebaseApp.configure() - + // For iOS 10 display notification (sent via APNS) UNUserNotificationCenter.current().delegate = self - + let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] UNUserNotificationCenter.current().requestAuthorization( - options: authOptions, - completionHandler: {_, _ in }) - + options: authOptions, + completionHandler: {_, _ in }) + application.registerForRemoteNotifications() return true } - + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { // Called when a new scene session is being created. // Use this method to select a configuration to create the new scene with. return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } - + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { // Called when the user discards a scene session. // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. @@ -52,31 +52,57 @@ class AppDelegate: NSObject, UIApplicationDelegate { } extension AppDelegate : UNUserNotificationCenterDelegate { - - // Receive displayed notifications for iOS 10 devices. - func userNotificationCenter(_ center: UNUserNotificationCenter, - willPresent notification: UNNotification, - withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - iOSRepository.getCircularRepository().updateCirculars(returnNewCirculars: false, completionHandler: - { result, error in - if let errorReal = error { - print(errorReal.localizedDescription) - } - }) - // UI is updated automatically - } - - func userNotificationCenter(_ center: UNUserNotificationCenter, - didReceive response: UNNotificationResponse, - withCompletionHandler completionHandler: @escaping () -> Void) { - iOSRepository.getCircularRepository().updateCirculars(returnNewCirculars: false, completionHandler: - { result, error in - if let errorReal = error { - print(errorReal.localizedDescription) - } - }) - - completionHandler() - } + // Receive displayed notifications for iOS 10 devices. + func userNotificationCenter(_ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + let userInfo = notification.request.content.userInfo + + // Handle reminder + if ((userInfo["reminder"]) != nil) { + let circularDao = iOSRepository.getCircularDao() + let circular = circularDao.getCircular(id: userInfo["id"] as! Int64, school: userInfo["school"] as! Int32) + iOSRepository.getCircularDao().update(id: circular.id, school: circular.school, favourite: circular.favourite, reminder: false, completionHandler: {_,_ in }) + + // Show notification + completionHandler([.alert, .sound, .badge]) + return + } + + // Handle new circular notification + iOSRepository.getCircularRepository().updateCirculars(returnNewCirculars: false, completionHandler: + { result, error in + if let errorReal = error { + print(errorReal.localizedDescription) + } + }) + + + + // UI is updated automatically + completionHandler([]) + } + + // Handle user action + func userNotificationCenter(_ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void) { + let userInfo = response.notification.request.content.userInfo + + // Do nothing if it's a reminder + if ((userInfo["reminder"]) != nil) { + completionHandler() + return + } + + iOSRepository.getCircularRepository().updateCirculars(returnNewCirculars: false, completionHandler: + { result, error in + if let errorReal = error { + print(errorReal.localizedDescription) + } + }) + + completionHandler() + } } diff --git a/ios/circolapp/circolapp/CircularViewModel.swift b/ios/circolapp/circolapp/CircularViewModel.swift index b5d5678..5097251 100644 --- a/ios/circolapp/circolapp/CircularViewModel.swift +++ b/ios/circolapp/circolapp/CircularViewModel.swift @@ -73,7 +73,7 @@ class CircularViewModel: ObservableObject { func startObservingAlarms() { stopObserving() - circularWatcher = repository.circularDao.getRemindersC(school: Int32(schoolID)).watch { circulars in + circularWatcher = repository.circularDao.getCFlowReminders(school: Int32(schoolID)).watch { circulars in self.circulars = circulars as! Array; } } diff --git a/ios/circolapp/circolapp/SceneDelegate.swift b/ios/circolapp/circolapp/SceneDelegate.swift index 8a3c473..31aed86 100644 --- a/ios/circolapp/circolapp/SceneDelegate.swift +++ b/ios/circolapp/circolapp/SceneDelegate.swift @@ -9,17 +9,17 @@ import UIKit import SwiftUI class SceneDelegate: UIResponder, UIWindowSceneDelegate { - + var window: UIWindow? - - + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). // Create the SwiftUI view that provides the window contents. let contentView = ContentView() - + // Use a UIHostingController as window root view controller. if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) @@ -28,29 +28,49 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window.makeKeyAndVisible() } } - + func sceneDidDisconnect(_ scene: UIScene) { // Called as the scene is being released by the system. // This occurs shortly after the scene enters the background, or when its session is discarded. // Release any resources associated with this scene that can be re-created the next time the scene connects. // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). } - + func sceneDidBecomeActive(_ scene: UIScene) { // Called when the scene has moved from an inactive state to an active state. // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. } - + func sceneWillResignActive(_ scene: UIScene) { // Called when the scene will move from an active state to an inactive state. // This may occur due to temporary interruptions (ex. an incoming phone call). } - + func sceneWillEnterForeground(_ scene: UIScene) { // Called as the scene transitions from the background to the foreground. // Use this method to undo the changes made on entering the background. - } + + let schoolID = UserDefaults.standard.integer(forKey: "school") + let reminders = iOSRepository.getCircularDao().getReminders(school: Int32(schoolID)) + // Remove reminder flag if notification was delivered + UNUserNotificationCenter.current().getPendingNotificationRequests { notifications in + // This function has to be called from the main thread because it is where database is accessible + DispatchQueue.main.async { + loop: + for circular in reminders { + for notification in notifications { + if (String(circular.id) == notification.identifier) { + continue loop + } + } + + iOSRepository.getCircularDao().update(id: circular.id, school: circular.school, favourite: circular.favourite, reminder: false, completionHandler: {_,_ in }) + } + } + } + } + func sceneDidEnterBackground(_ scene: UIScene) { // Called as the scene transitions from the foreground to the background. // Use this method to save data, release shared resources, and store enough scene-specific state information diff --git a/ios/circolapp/circolapp/View/CircularView.swift b/ios/circolapp/circolapp/View/CircularView.swift index 818656d..c0e24dd 100644 --- a/ios/circolapp/circolapp/View/CircularView.swift +++ b/ios/circolapp/circolapp/View/CircularView.swift @@ -21,6 +21,7 @@ import UIKit import Shared struct CircularView: View { + @State private var creatingReminder: Bool = false @State private var showDetail = false var circular: Circular @@ -99,7 +100,16 @@ struct CircularView: View { .buttonStyle(PlainButtonStyle()) Button(action: { - iOSRepository.getCircularDao().update(id: circular.id, school: circular.school, favourite: circular.favourite, reminder: !circular.reminder, completionHandler: {_,_ in }) + if circular.reminder { + let center = UNUserNotificationCenter.current() + center.removePendingNotificationRequests(withIdentifiers: [String(circular.id)]) + + iOSRepository.getCircularDao().update(id: circular.id, school: circular.school, favourite: circular.favourite, reminder: false, completionHandler: {_,_ in }) + + return + } + + self.creatingReminder = true }) { Image(systemName: circular.reminder ? "alarm.fill" : "alarm") .foregroundColor(.blue) @@ -107,10 +117,13 @@ struct CircularView: View { .padding() } .buttonStyle(PlainButtonStyle()) + .sheet(isPresented: self.$creatingReminder) { + NewReminderView(circular: circular) + } } ForEach(0... + */ + +import SwiftUI +import Shared + +struct NewReminderView: View { + @State private var reminderDate = Date() + @Environment(\.presentationMode) var presentationMode + var circular: Circular + + var body: some View { + NavigationView { + DatePicker("Pick a date for the reminder", selection: $reminderDate, in: Date()...) + .datePickerStyle(WheelDatePickerStyle()) + .labelsHidden() + .padding() + .navigationBarTitle(Text("New reminder"), displayMode: .inline) + .navigationBarItems(leading: Button(action: { + self.presentationMode.wrappedValue.dismiss() + }) { + Text("Cancel") + }, trailing: Button(action: { + createReminder() + self.presentationMode.wrappedValue.dismiss() + }) { + Text("Done") + }) + } + } + + func createReminder() { + iOSRepository.getCircularDao().update(id: circular.id, school: circular.school, favourite: circular.favourite, reminder: true, completionHandler: {_,_ in }) + + let center = UNUserNotificationCenter.current() + + let content = UNMutableNotificationContent() + content.title = "Circular number " + String(circular.id) + content.body = circular.name + content.sound = UNNotificationSound.default + content.userInfo["reminder"] = true + content.userInfo["id"] = circular.id + content.userInfo["school"] = circular.school + + let comps = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: reminderDate) + let trigger = UNCalendarNotificationTrigger(dateMatching: comps, repeats: false) + + let request = UNNotificationRequest(identifier: String(circular.id), content: content, trigger: trigger) + + center.add(request) { (error) in + if error != nil { + print("Error \(String(describing: error))") + } + } + } +} + +struct NewReminderView_Previews: PreviewProvider { + static var previewCircular = Circular(id: 1, school: 0, name: "This is a circular", url: "http://example.com", date: "19/11/2020", favourite: false, reminder: false, attachmentsNames: [], attachmentsUrls: []) + + static var previews: some View { + NewReminderView(circular: previewCircular) + } +} diff --git a/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/data/CircularDao.kt b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/data/CircularDao.kt index 97c6450..a585dde 100644 --- a/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/data/CircularDao.kt +++ b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/data/CircularDao.kt @@ -83,9 +83,10 @@ class CircularDao( .mapToList() fun searchFavouritesC(query: String, school: Int) = searchFavourites(query, school).wrap() - fun getReminders(school: Int) = + fun getReminders(school: Int) = appDatabaseQueries.getReminders(school.toLong(), circularMapper).executeAsList() + fun getFlowReminders(school: Int) = appDatabaseQueries.getReminders(school.toLong(), circularMapper).asFlow().mapToList() - fun getRemindersC(school: Int) = getReminders(school).wrap() + fun getCFlowReminders(school: Int) = getFlowReminders(school).wrap() fun searchReminders(query: String, school: Int) = appDatabaseQueries.searchReminders(school.toLong(), query, circularMapper).asFlow()