|
@@ -10,6 +10,7 @@ import Combine
|
|
|
import CryptoKit
|
|
import CryptoKit
|
|
|
import StoreKit
|
|
import StoreKit
|
|
|
import SwiftUI
|
|
import SwiftUI
|
|
|
|
|
+import UserNotifications
|
|
|
import WebKit
|
|
import WebKit
|
|
|
|
|
|
|
|
/// Default main window **content** size; OAuth sign-in uses the same dimensions and aligns to this content area.
|
|
/// Default main window **content** size; OAuth sign-in uses the same dimensions and aligns to this content area.
|
|
@@ -57,6 +58,11 @@ class ViewController: NSViewController {
|
|
|
private let zoomOAuth = ZoomOAuthService.shared
|
|
private let zoomOAuth = ZoomOAuthService.shared
|
|
|
private let loginStateKey = "zoom_app.isLoggedIn"
|
|
private let loginStateKey = "zoom_app.isLoggedIn"
|
|
|
private let darkModeDefaultsKey = "settings.darkModeEnabled"
|
|
private let darkModeDefaultsKey = "settings.darkModeEnabled"
|
|
|
|
|
+ private let legacyNotificationsEnabledDefaultsKey = "settings.notificationsEnabled"
|
|
|
|
|
+ private let remindersEnabledDefaultsKey = "reminders.enabled"
|
|
|
|
|
+ private let remind1DayDefaultsKey = "reminders.1day"
|
|
|
|
|
+ private let remind12HoursDefaultsKey = "reminders.12hours"
|
|
|
|
|
+ private let remind1HourDefaultsKey = "reminders.1hour"
|
|
|
private let ratingPromptShownKey = "zoom_app.ratingPromptShown"
|
|
private let ratingPromptShownKey = "zoom_app.ratingPromptShown"
|
|
|
private let ratingCompletedKey = "zoom_app.ratingCompleted"
|
|
private let ratingCompletedKey = "zoom_app.ratingCompleted"
|
|
|
private let usageSessionStartedAtKey = "zoom_app.usageSessionStartedAt"
|
|
private let usageSessionStartedAtKey = "zoom_app.usageSessionStartedAt"
|
|
@@ -210,6 +216,47 @@ class ViewController: NSViewController {
|
|
|
}
|
|
}
|
|
|
set { UserDefaults.standard.set(newValue, forKey: darkModeDefaultsKey) }
|
|
set { UserDefaults.standard.set(newValue, forKey: darkModeDefaultsKey) }
|
|
|
}
|
|
}
|
|
|
|
|
+ private var remindersEnabled: Bool {
|
|
|
|
|
+ get {
|
|
|
|
|
+ let defaults = UserDefaults.standard
|
|
|
|
|
+ if defaults.object(forKey: remindersEnabledDefaultsKey) != nil {
|
|
|
|
|
+ return defaults.bool(forKey: remindersEnabledDefaultsKey)
|
|
|
|
|
+ }
|
|
|
|
|
+ // Migration: if the old single-toggle key exists, treat it as master reminders toggle.
|
|
|
|
|
+ if defaults.object(forKey: legacyNotificationsEnabledDefaultsKey) != nil {
|
|
|
|
|
+ return defaults.bool(forKey: legacyNotificationsEnabledDefaultsKey)
|
|
|
|
|
+ }
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+ set { UserDefaults.standard.set(newValue, forKey: remindersEnabledDefaultsKey) }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var remind1Day: Bool {
|
|
|
|
|
+ get {
|
|
|
|
|
+ let defaults = UserDefaults.standard
|
|
|
|
|
+ let stored = defaults.object(forKey: remind1DayDefaultsKey)
|
|
|
|
|
+ return stored != nil ? defaults.bool(forKey: remind1DayDefaultsKey) : true
|
|
|
|
|
+ }
|
|
|
|
|
+ set { UserDefaults.standard.set(newValue, forKey: remind1DayDefaultsKey) }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var remind12Hours: Bool {
|
|
|
|
|
+ get {
|
|
|
|
|
+ let defaults = UserDefaults.standard
|
|
|
|
|
+ let stored = defaults.object(forKey: remind12HoursDefaultsKey)
|
|
|
|
|
+ return stored != nil ? defaults.bool(forKey: remind12HoursDefaultsKey) : true
|
|
|
|
|
+ }
|
|
|
|
|
+ set { UserDefaults.standard.set(newValue, forKey: remind12HoursDefaultsKey) }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var remind1Hour: Bool {
|
|
|
|
|
+ get {
|
|
|
|
|
+ let defaults = UserDefaults.standard
|
|
|
|
|
+ let stored = defaults.object(forKey: remind1HourDefaultsKey)
|
|
|
|
|
+ return stored != nil ? defaults.bool(forKey: remind1HourDefaultsKey) : true
|
|
|
|
|
+ }
|
|
|
|
|
+ set { UserDefaults.standard.set(newValue, forKey: remind1HourDefaultsKey) }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
private let sidebarWidth: CGFloat = 78
|
|
private let sidebarWidth: CGFloat = 78
|
|
|
private var appBackground: NSColor { palette.appBackground }
|
|
private var appBackground: NSColor { palette.appBackground }
|
|
@@ -302,6 +349,13 @@ class ViewController: NSViewController {
|
|
|
private var schedulerDateByButtonTag: [Int: Date] = [:]
|
|
private var schedulerDateByButtonTag: [Int: Date] = [:]
|
|
|
private var nextSchedulerButtonTag: Int = 1
|
|
private var nextSchedulerButtonTag: Int = 1
|
|
|
private weak var settingsDarkModeSwitch: NSSwitch?
|
|
private weak var settingsDarkModeSwitch: NSSwitch?
|
|
|
|
|
+ private weak var settingsRemindersMasterSwitch: NSSwitch?
|
|
|
|
|
+ private weak var settingsReminders1DaySwitch: NSSwitch?
|
|
|
|
|
+ private weak var settingsReminders12HoursSwitch: NSSwitch?
|
|
|
|
|
+ private weak var settingsReminders1HourSwitch: NSSwitch?
|
|
|
|
|
+ private weak var settingsReminders1DayRow: NSView?
|
|
|
|
|
+ private weak var settingsReminders12HoursRow: NSView?
|
|
|
|
|
+ private weak var settingsReminders1HourRow: NSView?
|
|
|
private weak var settingsUpgradeButton: NSButton?
|
|
private weak var settingsUpgradeButton: NSButton?
|
|
|
private weak var settingsUpgradeTitleLabel: NSTextField?
|
|
private weak var settingsUpgradeTitleLabel: NSTextField?
|
|
|
private weak var settingsGoogleActionButton: NSButton?
|
|
private weak var settingsGoogleActionButton: NSButton?
|
|
@@ -2401,6 +2455,7 @@ class ViewController: NSViewController {
|
|
|
applyFilteredMeetings()
|
|
applyFilteredMeetings()
|
|
|
applyMeetingsPageFilters(resetVisible: true)
|
|
applyMeetingsPageFilters(resetVisible: true)
|
|
|
updateWidgetMeetingStoreFromCurrentMeetings()
|
|
updateWidgetMeetingStoreFromCurrentMeetings()
|
|
|
|
|
+ rescheduleMeetingRemindersIfNeeded()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@MainActor
|
|
@MainActor
|
|
@@ -3338,6 +3393,13 @@ class ViewController: NSViewController {
|
|
|
|
|
|
|
|
stack.setCustomSpacing(24, after: upgradeButton)
|
|
stack.setCustomSpacing(24, after: upgradeButton)
|
|
|
|
|
|
|
|
|
|
+ let notificationsTitle = textLabel("Notifications", font: .systemFont(ofSize: 16, weight: .semibold), color: primaryText)
|
|
|
|
|
+ stack.addArrangedSubview(notificationsTitle)
|
|
|
|
|
+ let remindersSection = makeSettingsRemindersSection()
|
|
|
|
|
+ stack.addArrangedSubview(remindersSection)
|
|
|
|
|
+ remindersSection.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
|
|
|
|
|
+ stack.setCustomSpacing(24, after: remindersSection)
|
|
|
|
|
+
|
|
|
let legalTitle = textLabel("Help & Legal", font: .systemFont(ofSize: 16, weight: .semibold), color: primaryText)
|
|
let legalTitle = textLabel("Help & Legal", font: .systemFont(ofSize: 16, weight: .semibold), color: primaryText)
|
|
|
stack.addArrangedSubview(legalTitle)
|
|
stack.addArrangedSubview(legalTitle)
|
|
|
let privacyButton = makeSettingsActionButton(icon: "🔒", title: "Privacy Policy", action: .privacyPolicy)
|
|
let privacyButton = makeSettingsActionButton(icon: "🔒", title: "Privacy Policy", action: .privacyPolicy)
|
|
@@ -3417,6 +3479,115 @@ class ViewController: NSViewController {
|
|
|
return row
|
|
return row
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private func makeSettingsRemindersSection() -> NSView {
|
|
|
|
|
+ let container = NSStackView()
|
|
|
|
|
+ container.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ container.orientation = .vertical
|
|
|
|
|
+ container.spacing = 1
|
|
|
|
|
+ container.alignment = .leading
|
|
|
|
|
+
|
|
|
|
|
+ let masterRow = makeReminderToggleRow(
|
|
|
|
|
+ icon: "🔔",
|
|
|
|
|
+ title: "Reminders",
|
|
|
|
|
+ state: remindersEnabled,
|
|
|
|
|
+ action: #selector(settingsReminderMasterToggled(_:)),
|
|
|
|
|
+ isSubRow: false
|
|
|
|
|
+ )
|
|
|
|
|
+ container.addArrangedSubview(masterRow)
|
|
|
|
|
+ masterRow.widthAnchor.constraint(equalTo: container.widthAnchor).isActive = true
|
|
|
|
|
+
|
|
|
|
|
+ let subAlpha: CGFloat = remindersEnabled ? 1.0 : 0.4
|
|
|
|
|
+
|
|
|
|
|
+ let day1Row = makeReminderToggleRow(
|
|
|
|
|
+ icon: "📅",
|
|
|
|
|
+ title: "1 Day Before",
|
|
|
|
|
+ state: remind1Day,
|
|
|
|
|
+ action: #selector(settingsReminder1DayToggled(_:)),
|
|
|
|
|
+ isSubRow: true
|
|
|
|
|
+ )
|
|
|
|
|
+ day1Row.alphaValue = subAlpha
|
|
|
|
|
+ container.addArrangedSubview(day1Row)
|
|
|
|
|
+ day1Row.widthAnchor.constraint(equalTo: container.widthAnchor).isActive = true
|
|
|
|
|
+
|
|
|
|
|
+ let hours12Row = makeReminderToggleRow(
|
|
|
|
|
+ icon: "🕛",
|
|
|
|
|
+ title: "12 Hours Before",
|
|
|
|
|
+ state: remind12Hours,
|
|
|
|
|
+ action: #selector(settingsReminder12HoursToggled(_:)),
|
|
|
|
|
+ isSubRow: true
|
|
|
|
|
+ )
|
|
|
|
|
+ hours12Row.alphaValue = subAlpha
|
|
|
|
|
+ container.addArrangedSubview(hours12Row)
|
|
|
|
|
+ hours12Row.widthAnchor.constraint(equalTo: container.widthAnchor).isActive = true
|
|
|
|
|
+
|
|
|
|
|
+ let hour1Row = makeReminderToggleRow(
|
|
|
|
|
+ icon: "⏰",
|
|
|
|
|
+ title: "1 Hour Before",
|
|
|
|
|
+ state: remind1Hour,
|
|
|
|
|
+ action: #selector(settingsReminder1HourToggled(_:)),
|
|
|
|
|
+ isSubRow: true
|
|
|
|
|
+ )
|
|
|
|
|
+ hour1Row.alphaValue = subAlpha
|
|
|
|
|
+ container.addArrangedSubview(hour1Row)
|
|
|
|
|
+ hour1Row.widthAnchor.constraint(equalTo: container.widthAnchor).isActive = true
|
|
|
|
|
+
|
|
|
|
|
+ // Keep references so toggles can update UI without rebuilding the whole settings view.
|
|
|
|
|
+ if let masterSwitch = (masterRow.subviews.compactMap { $0 as? NSSwitch }.first) {
|
|
|
|
|
+ settingsRemindersMasterSwitch = masterSwitch
|
|
|
|
|
+ }
|
|
|
|
|
+ if let day1Switch = (day1Row.subviews.compactMap { $0 as? NSSwitch }.first) {
|
|
|
|
|
+ settingsReminders1DaySwitch = day1Switch
|
|
|
|
|
+ }
|
|
|
|
|
+ if let hours12Switch = (hours12Row.subviews.compactMap { $0 as? NSSwitch }.first) {
|
|
|
|
|
+ settingsReminders12HoursSwitch = hours12Switch
|
|
|
|
|
+ }
|
|
|
|
|
+ if let hour1Switch = (hour1Row.subviews.compactMap { $0 as? NSSwitch }.first) {
|
|
|
|
|
+ settingsReminders1HourSwitch = hour1Switch
|
|
|
|
|
+ }
|
|
|
|
|
+ settingsReminders1DayRow = day1Row
|
|
|
|
|
+ settingsReminders12HoursRow = hours12Row
|
|
|
|
|
+ settingsReminders1HourRow = hour1Row
|
|
|
|
|
+
|
|
|
|
|
+ return container
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func makeReminderToggleRow(
|
|
|
|
|
+ icon: String,
|
|
|
|
|
+ title: String,
|
|
|
|
|
+ state: Bool,
|
|
|
|
|
+ action: Selector,
|
|
|
|
|
+ isSubRow: Bool
|
|
|
|
|
+ ) -> NSView {
|
|
|
|
|
+ let row = roundedContainer(cornerRadius: 10, color: palette.inputBackground)
|
|
|
|
|
+ row.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ row.heightAnchor.constraint(equalToConstant: 52).isActive = true
|
|
|
|
|
+ styleSurface(row, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
|
|
|
|
|
+
|
|
|
|
|
+ let iconLabel = textLabel(icon, font: NSFont.systemFont(ofSize: 18, weight: .medium), color: primaryText)
|
|
|
|
|
+ let titleLabel = textLabel(title, font: NSFont.systemFont(ofSize: 15, weight: isSubRow ? .medium : .semibold), color: primaryText)
|
|
|
|
|
+
|
|
|
|
|
+ let toggle = NSSwitch()
|
|
|
|
|
+ toggle.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ toggle.state = state ? .on : .off
|
|
|
|
|
+ toggle.target = self
|
|
|
|
|
+ toggle.action = action
|
|
|
|
|
+
|
|
|
|
|
+ row.addSubview(iconLabel)
|
|
|
|
|
+ row.addSubview(titleLabel)
|
|
|
|
|
+ row.addSubview(toggle)
|
|
|
|
|
+
|
|
|
|
|
+ let leading: CGFloat = isSubRow ? 26 : 14
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ iconLabel.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: leading),
|
|
|
|
|
+ iconLabel.centerYAnchor.constraint(equalTo: row.centerYAnchor),
|
|
|
|
|
+ titleLabel.leadingAnchor.constraint(equalTo: iconLabel.trailingAnchor, constant: 10),
|
|
|
|
|
+ titleLabel.centerYAnchor.constraint(equalTo: row.centerYAnchor),
|
|
|
|
|
+ toggle.trailingAnchor.constraint(equalTo: row.trailingAnchor, constant: -14),
|
|
|
|
|
+ toggle.centerYAnchor.constraint(equalTo: row.centerYAnchor)
|
|
|
|
|
+ ])
|
|
|
|
|
+ return row
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private func makeSettingsGoogleAccountRow() -> NSView {
|
|
private func makeSettingsGoogleAccountRow() -> NSView {
|
|
|
let row = roundedContainer(cornerRadius: 10, color: palette.inputBackground)
|
|
let row = roundedContainer(cornerRadius: 10, color: palette.inputBackground)
|
|
|
row.translatesAutoresizingMaskIntoConstraints = false
|
|
row.translatesAutoresizingMaskIntoConstraints = false
|
|
@@ -3691,6 +3862,152 @@ class ViewController: NSViewController {
|
|
|
setDarkMode(sender.state == .on)
|
|
setDarkMode(sender.state == .on)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ @objc private func settingsReminderMasterToggled(_ sender: NSSwitch) {
|
|
|
|
|
+ let enabled = sender.state == .on
|
|
|
|
|
+ remindersEnabled = enabled
|
|
|
|
|
+ updateRemindersSubRowState()
|
|
|
|
|
+
|
|
|
|
|
+ if enabled {
|
|
|
|
|
+ requestNotificationPermissionIfNeeded { [weak self] granted in
|
|
|
|
|
+ guard let self else { return }
|
|
|
|
|
+ if granted {
|
|
|
|
|
+ self.rescheduleMeetingRemindersIfNeeded()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ self.remindersEnabled = false
|
|
|
|
|
+ self.settingsRemindersMasterSwitch?.state = .off
|
|
|
|
|
+ self.updateRemindersSubRowState()
|
|
|
|
|
+ self.cancelAllMeetingReminders()
|
|
|
|
|
+ self.showSimpleAlert(
|
|
|
|
|
+ title: "Notifications disabled",
|
|
|
|
|
+ message: "To enable reminders, allow notifications for this app in System Settings → Notifications."
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ cancelAllMeetingReminders()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @objc private func settingsReminder1DayToggled(_ sender: NSSwitch) {
|
|
|
|
|
+ remind1Day = sender.state == .on
|
|
|
|
|
+ rescheduleMeetingRemindersIfNeeded()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @objc private func settingsReminder12HoursToggled(_ sender: NSSwitch) {
|
|
|
|
|
+ remind12Hours = sender.state == .on
|
|
|
|
|
+ rescheduleMeetingRemindersIfNeeded()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @objc private func settingsReminder1HourToggled(_ sender: NSSwitch) {
|
|
|
|
|
+ remind1Hour = sender.state == .on
|
|
|
|
|
+ rescheduleMeetingRemindersIfNeeded()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func updateRemindersSubRowState() {
|
|
|
|
|
+ let subAlpha: CGFloat = remindersEnabled ? 1.0 : 0.4
|
|
|
|
|
+ settingsReminders1DayRow?.alphaValue = subAlpha
|
|
|
|
|
+ settingsReminders12HoursRow?.alphaValue = subAlpha
|
|
|
|
|
+ settingsReminders1HourRow?.alphaValue = subAlpha
|
|
|
|
|
+
|
|
|
|
|
+ let enabled = remindersEnabled
|
|
|
|
|
+ settingsReminders1DaySwitch?.isEnabled = enabled
|
|
|
|
|
+ settingsReminders12HoursSwitch?.isEnabled = enabled
|
|
|
|
|
+ settingsReminders1HourSwitch?.isEnabled = enabled
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func requestNotificationPermissionIfNeeded(completion: @escaping (Bool) -> Void) {
|
|
|
|
|
+ let center = UNUserNotificationCenter.current()
|
|
|
|
|
+ center.getNotificationSettings { settings in
|
|
|
|
|
+ switch settings.authorizationStatus {
|
|
|
|
|
+ case .notDetermined:
|
|
|
|
|
+ center.requestAuthorization(options: [.alert, .sound]) { granted, _ in
|
|
|
|
|
+ DispatchQueue.main.async { completion(granted) }
|
|
|
|
|
+ }
|
|
|
|
|
+ case .authorized, .provisional, .ephemeral:
|
|
|
|
|
+ DispatchQueue.main.async { completion(true) }
|
|
|
|
|
+ default:
|
|
|
|
|
+ DispatchQueue.main.async { completion(false) }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func rescheduleMeetingRemindersIfNeeded() {
|
|
|
|
|
+ guard remindersEnabled else {
|
|
|
|
|
+ cancelAllMeetingReminders()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ // If nothing is selected, don't schedule anything (but keep master toggle on).
|
|
|
|
|
+ if remind1Day == false, remind12Hours == false, remind1Hour == false {
|
|
|
|
|
+ cancelAllMeetingReminders()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ scheduleMeetingReminders(for: allScheduledMeetings)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func cancelAllMeetingReminders() {
|
|
|
|
|
+ UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func scheduleMeetingReminders(for meetings: [ScheduledMeeting]) {
|
|
|
|
|
+ let center = UNUserNotificationCenter.current()
|
|
|
|
|
+ let now = Date()
|
|
|
|
|
+ struct Interval {
|
|
|
|
|
+ let offset: TimeInterval
|
|
|
|
|
+ let label: String
|
|
|
|
|
+ let suffix: String
|
|
|
|
|
+ let isEnabled: () -> Bool
|
|
|
|
|
+ }
|
|
|
|
|
+ let intervals: [Interval] = [
|
|
|
|
|
+ Interval(offset: 60 * 60 * 24, label: "Starts in 1 day", suffix: "1d", isEnabled: { self.remind1Day }),
|
|
|
|
|
+ Interval(offset: 60 * 60 * 12, label: "Starts in 12 hours", suffix: "12h", isEnabled: { self.remind12Hours }),
|
|
|
|
|
+ Interval(offset: 60 * 60, label: "Starts in 1 hour", suffix: "1h", isEnabled: { self.remind1Hour })
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ var requests: [UNNotificationRequest] = []
|
|
|
|
|
+ for meeting in meetings {
|
|
|
|
|
+ let title = meeting.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
|
+ guard title.isEmpty == false else { continue }
|
|
|
|
|
+
|
|
|
|
|
+ for interval in intervals where interval.isEnabled() {
|
|
|
|
|
+ let triggerDate = meeting.start.addingTimeInterval(-interval.offset)
|
|
|
|
|
+ guard triggerDate > now else { continue }
|
|
|
|
|
+
|
|
|
|
|
+ let content = UNMutableNotificationContent()
|
|
|
|
|
+ content.title = "📅 \(title)"
|
|
|
|
|
+ let subtitle = meeting.host.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
|
+ content.subtitle = subtitle.isEmpty ? meeting.source : subtitle
|
|
|
|
|
+ content.body = interval.label
|
|
|
|
|
+ content.sound = .default
|
|
|
|
|
+
|
|
|
|
|
+ let components = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute, .second], from: triggerDate)
|
|
|
|
|
+ let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false)
|
|
|
|
|
+ let id = "zoom.meeting.reminder.\(stableReminderKey(for: meeting)).\(interval.suffix)"
|
|
|
|
|
+ requests.append(UNNotificationRequest(identifier: id, content: content, trigger: trigger))
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ center.removeAllPendingNotificationRequests()
|
|
|
|
|
+ for request in requests {
|
|
|
|
|
+ center.add(request, withCompletionHandler: nil)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func stableReminderKey(for meeting: ScheduledMeeting) -> String {
|
|
|
|
|
+ if let id = meeting.meetingID?.trimmingCharacters(in: .whitespacesAndNewlines), id.isEmpty == false {
|
|
|
|
|
+ return id
|
|
|
|
|
+ }
|
|
|
|
|
+ if let url = meeting.webURL?.absoluteString, url.isEmpty == false {
|
|
|
|
|
+ return sha256Prefix(url)
|
|
|
|
|
+ }
|
|
|
|
|
+ return sha256Prefix("\(meeting.title)|\(meeting.start.timeIntervalSince1970)")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func sha256Prefix(_ input: String) -> String {
|
|
|
|
|
+ let digest = SHA256.hash(data: Data(input.utf8))
|
|
|
|
|
+ let hex = digest.compactMap { String(format: "%02x", $0) }.joined()
|
|
|
|
|
+ return String(hex.prefix(24))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private func setDarkMode(_ enabled: Bool) {
|
|
private func setDarkMode(_ enabled: Bool) {
|
|
|
darkModeEnabled = enabled
|
|
darkModeEnabled = enabled
|
|
|
palette = Palette(isDarkMode: enabled)
|
|
palette = Palette(isDarkMode: enabled)
|