import Foundation import UserNotifications /** * Local reminders before each lesson of the coming week, from the saved timetable: they work * offline, no push server is involved. iOS keeps at most 64 pending requests per app. */ enum Reminders { private static let prefix = "lesson-" private static let limit = 48 /** Asks for permission; false when the student declined. */ static func authorize() async -> Bool { (try? await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge])) ?? false } /** Replaces every scheduled reminder with the ones the timetable needs now. */ static func reschedule(timetable: Timetable?, settings: UserSettings) { let center = UNUserNotificationCenter.current() Task { let pending = await center.pendingNotificationRequests().map(\.identifier).filter { $0.hasPrefix(prefix) } center.removePendingNotificationRequests(withIdentifiers: pending) guard settings.reminders, let t = timetable else { return } let now = Date() // A week ahead, so a Friday evening already holds Monday's reminders. let horizon = now.addingTimeInterval(7 * 24 * 3600) var count = 0 for day in t.days { guard let date = CivilDate(day.date), count < limit else { continue } // One reminder per start time: parallel subgroups share the notification. for slot in t.slots(on: date, subgroup: settings.subgroup, hidden: settings.hidden) { let begins = date.at(slot.start) let at = begins.addingTimeInterval(-Double(settings.reminderMinutes) * 60) guard at > now, begins < horizon, count < limit else { continue } let content = UNMutableNotificationContent() content.title = slot.lessons.map(\.subject).joined(separator: " / ") let first = slot.lessons[0] let rooms = Array(Set(slot.lessons.flatMap(\.rooms))).sorted().joined(separator: ", ") let place = withPlace(rooms.isEmpty ? nil : "ауд. " + rooms, slot.lessons.compactMap(\.place).first) var text = slot.start.description if let pair = first.pair { text += " · \(pair) пара" } if let place { text += " · \(place)" } if slot.lessons.contains(where: \.uncertain) { text += " · сверьте недели" } content.body = text content.sound = .default content.interruptionLevel = .timeSensitive let parts = minskCalendar.dateComponents(in: minsk, from: at) let trigger = UNCalendarNotificationTrigger( dateMatching: DateComponents(timeZone: minsk, year: parts.year, month: parts.month, day: parts.day, hour: parts.hour, minute: parts.minute), repeats: false ) try? await center.add(UNNotificationRequest(identifier: prefix + slot.id, content: content, trigger: trigger)) count += 1 } } } } }