Added notifications for visiting chefs

You can now get notified when visiting chefs are on campus! A menu is available from the toolbar on the main screen that allows you to enable notifications and configure what visiting chefs you want to be notified for. You can also toggle if you want to be notified 1, 2, or 3 hours before the chef arrives on campus.
Other changes in this commit:
- Updated maps URL to be compatible with the new RIT map (however they don't open correctly- this is outside of my control)
- Removed button linking to OnDemand at the request of RIT ITS
This commit is contained in:
2025-12-08 01:45:30 -05:00
parent d7096980d7
commit 207fa788e1
11 changed files with 371 additions and 91 deletions

View File

@@ -6,5 +6,54 @@
//
import Foundation
import UserNotifications
/// Function to schedule a notification for a visting chef showing up on campus using the name, location, and timeframe. Returns the UUID string assigned to the notification.
func scheduleVisitingChefNotif(name: String, location: String, startTime: Date, endTime: Date) async -> String {
// Validate that the user has authorized TigerDine to send you notifications before trying to schedule one.
let center = UNUserNotificationCenter.current()
let settings = await center.notificationSettings()
guard (settings.authorizationStatus == .authorized) else { return "" }
// Build the notification content from the name, location, and timeframe.
let content = UNMutableNotificationContent()
if name == "P.H. Express" {
content.title = "Good Food is Waiting"
} else {
content.title = "\(name) Is On Campus Today"
}
content.body = "\(name) will be at \(location) from \(dateDisplay.string(from: startTime))-\(dateDisplay.string(from: endTime))"
// Get the time that we're going to schedule the notification for, which is a specified number of hours before the chef
// shows up. This is configurable from the notification settings.
let offset: Int = UserDefaults.standard.integer(forKey: "notificationOffset")
// The ternary happening on this line is stupid, but the UserDefaults key isn't always initialized because it's being used
// through @AppStorage. This will eventually be refactored into something better, but this system should work for now to
// ensure that we never use an offset of 0.
let notifTime = Calendar.current.date(byAdding: .hour, value: -(offset != 0 ? offset : 2), to: startTime)!
let notifTimeComponents = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: notifTime)
let trigger = UNCalendarNotificationTrigger(dateMatching: notifTimeComponents, repeats: false)
// Create the request with the content and time.
let uuid_string = UUID().uuidString
let request = UNNotificationRequest(identifier: uuid_string, content: content, trigger: trigger)
// Hook into the notification center and attempt to schedule the notification.
let notificationCenter = UNUserNotificationCenter.current()
do {
try await notificationCenter.add(request)
print("successfully scheduled notification for chef \(name) to be delivered at \(notifTime)")
return uuid_string
} catch {
// Presumably this shouldn't ever happen? That's what I'm hoping for!
print(error)
return ""
}
}
/// Cancel a list of pending visiting chef notifications using their UUIDs.
func cancelVisitingChefNotifs(uuids: [String]) async {
let center = UNUserNotificationCenter.current()
center.removePendingNotificationRequests(withIdentifiers: uuids)
print("successfully cancelled pending notifications with UUIDs: \(uuids)")
}

View File

@@ -47,6 +47,10 @@ func parseLocationInfo(location: DiningLocationParser, forDate: Date?) -> Dining
nil
}
// Generate a maps URL from the mdoId key. This is required because the mapsUrl served by TigerCenter is not compatible with
// the new RIT map that was deployed in December 2025.
let mapsUrl = "https://maps.rit.edu/details/\(location.mdoId)"
// Early return if there are no events, good for things like the food trucks which can very easily have no openings in a week.
if location.events.isEmpty {
return DiningLocation(
@@ -56,7 +60,7 @@ func parseLocationInfo(location: DiningLocationParser, forDate: Date?) -> Dining
name: location.name,
summary: location.summary,
desc: desc,
mapsUrl: location.mapsUrl,
mapsUrl: mapsUrl,
date: forDate ?? Date(),
diningTimes: nil,
open: .closed,
@@ -102,7 +106,7 @@ func parseLocationInfo(location: DiningLocationParser, forDate: Date?) -> Dining
name: location.name,
summary: location.summary,
desc: desc,
mapsUrl: location.mapsUrl,
mapsUrl: mapsUrl,
date: forDate ?? Date(),
diningTimes: nil,
open: .closed,
@@ -179,7 +183,7 @@ func parseLocationInfo(location: DiningLocationParser, forDate: Date?) -> Dining
print("found visiting chef: \(menu.name)")
var name: String = menu.name
let splitString = name.split(separator: "(", maxSplits: 1)
name = String(splitString[0])
name = String(splitString[0]).trimmingCharacters(in: .whitespaces)
// Time parsing nonsense starts here. Extracts the time from a string like "Chef (4-7p.m.)", splits it at the "-",
// strips the non-numerical characters from each part, parses it as a number and adds 12 hours as needed, then creates
// a Date instance for that time on today's date.
@@ -199,7 +203,7 @@ func parseLocationInfo(location: DiningLocationParser, forDate: Date?) -> Dining
bySettingHour: openTimeComponents.hour!,
minute: openTimeComponents.minute!,
second: openTimeComponents.second!,
of: now)!
of: forDate ?? now)!
} else {
break
}
@@ -212,7 +216,7 @@ func parseLocationInfo(location: DiningLocationParser, forDate: Date?) -> Dining
bySettingHour: closeTimeComponents.hour!,
minute: closeTimeComponents.minute!,
second: closeTimeComponents.second!,
of: now)!
of: forDate ?? now)!
} else {
break
}
@@ -261,7 +265,7 @@ func parseLocationInfo(location: DiningLocationParser, forDate: Date?) -> Dining
name: location.name,
summary: location.summary,
desc: desc,
mapsUrl: location.mapsUrl,
mapsUrl: mapsUrl,
date: forDate ?? Date(),
diningTimes: diningTimes,
open: openStatus,