max / goingson
| 1 | //! What GoingsOn can interrupt you about, declared once. |
| 2 | //! |
| 3 | //! The registry [`quasi_notifs`] is built around: three kinds, each carrying |
| 4 | //! everything the settings pane, the config store and the watcher need. |
| 5 | //! |
| 6 | //! # Why this module is not inside `notifications` |
| 7 | //! |
| 8 | //! That module is desktop-only — `#[cfg(not(any(target_os = "ios", target_os = |
| 9 | //! "android")))]` — because delivery is. The declaration is not: the generated |
| 10 | //! keys are part of [`crate::config_key::CONFIG`] on every platform, or a phone |
| 11 | //! would sync a table whose closed set it disagreed with, and refuse the rows |
| 12 | //! the desktop wrote. |
| 13 | //! |
| 14 | //! # Which kinds ship on |
| 15 | //! |
| 16 | //! The first three declare [`shipping_on`](Kind::shipping_on). **A new kind |
| 17 | //! ships off**, and onboarding points at it: the pointer counts the kinds that |
| 18 | //! ship off and that nobody has answered yet, over |
| 19 | //! [`crate::commands::list_notification_kinds`]. |
| 20 | //! |
| 21 | //! Those three carry no [`Knob`](quasi_notifs::Knob), and that is measured |
| 22 | //! rather than deferred: the one lead time in this app belongs to the *event* |
| 23 | //! (`event.reminder_offsets_seconds`, several per event), which is the exact |
| 24 | //! case `Setting`'s header says is not a knob, and the other lead time in the |
| 25 | //! settings pane is `event_lead_minutes`, which is not about notifications at |
| 26 | //! all. See [`crate::config_key`] for the collision that dotted keys settle. |
| 27 | //! |
| 28 | //! # The fourth, which needed a scheduler |
| 29 | //! |
| 30 | //! **Task due-date reminders** needs a pass of its own in the watcher and a |
| 31 | //! lead time to compare against. It ships off, and it carries the one |
| 32 | //! [`Knob`](quasi_notifs::Knob) GoingsOn declares. |
| 33 | //! |
| 34 | //! <!-- wiki: quasi-overview --> |
| 35 | |
| 36 | use ; |
| 37 | |
| 38 | /// The snooze-expiry kind's id, and the token stem its occurrences use. |
| 39 | pub const SNOOZE_EXPIRY: &str = "snooze-expiry"; |
| 40 | /// The overdue-response kind's id. |
| 41 | pub const RESPONSE_OVERDUE: &str = "response-overdue"; |
| 42 | /// The event-reminder kind's id. |
| 43 | pub const EVENT_REMINDER: &str = "event-reminder"; |
| 44 | /// The task-due kind's id. |
| 45 | pub const TASK_DUE: &str = "task-due"; |
| 46 | /// The task-due kind's lead-time knob. |
| 47 | pub const TASK_DUE_LEAD: &str = "lead"; |
| 48 | |
| 49 | /// How long before a task is due it says so, before anyone chooses. |
| 50 | /// |
| 51 | /// An hour. A task's `due` is a full instant rather than a date (the form |
| 52 | /// parses "friday 3pm"), so a lead measured in days would fire at a time that |
| 53 | /// has nothing to do with the deadline, and one measured in minutes is a |
| 54 | /// reminder you cannot act on. An hour is long enough to do something and short |
| 55 | /// enough to still be about today. |
| 56 | const TASK_DUE_LEAD_DEFAULT_SECONDS: i64 = 60 * 60; |
| 57 | |
| 58 | /// The task-due kind's knobs. One: how far ahead it speaks up. |
| 59 | static TASK_DUE_KNOBS: & = & |
| 60 | TASK_DUE_LEAD, |
| 61 | "Lead time (seconds)", |
| 62 | Seconds, |
| 63 | ]; |
| 64 | |
| 65 | /// Every kind GoingsOn declares. |
| 66 | /// |
| 67 | /// One category, because all three are the same promise: something you already |
| 68 | /// asked to hear about, at the moment it comes due. A second category would be |
| 69 | /// a second heading in the pane over one control, which reads as structure that |
| 70 | /// is not there. |
| 71 | static KINDS: & = & |
| 72 | new |
| 73 | SNOOZE_EXPIRY, |
| 74 | "Snoozed items resurface", |
| 75 | "When a task or email you snoozed comes back.", |
| 76 | "Reminders", |
| 77 | |
| 78 | .shipping_on, |
| 79 | new |
| 80 | RESPONSE_OVERDUE, |
| 81 | "Replies you are still waiting for", |
| 82 | "When something you marked as waiting passes the date you expected it.", |
| 83 | "Reminders", |
| 84 | |
| 85 | .shipping_on, |
| 86 | // The one kind that is quiet after a restart, and the reason `CatchUp` is |
| 87 | // per kind rather than per app: a snooze that ran out overnight is still |
| 88 | // wanted, and a reminder for a meeting that started an hour ago is an |
| 89 | // interruption about something already missed. The shipped watcher said |
| 90 | // this with a `reminders_bootstrapped` flag it had to reset by hand |
| 91 | // whenever it cleared its memory; the declaration says it once. |
| 92 | new |
| 93 | EVENT_REMINDER, |
| 94 | "Event reminders", |
| 95 | "Before an event starts, at the lead times that event carries.", |
| 96 | "Reminders", |
| 97 | |
| 98 | .shipping_on |
| 99 | .quiet_after_restart, |
| 100 | // The one kind that ships off, because it is new and every new kind does. |
| 101 | // It takes the default `CatchUp::Fire` rather than `quiet_after_restart`, |
| 102 | // and the contrast with the kind above it is the whole of what `CatchUp` |
| 103 | // is for: a meeting that started while the app was shut is a moment that |
| 104 | // has passed, and a deadline that passed while the app was shut is still |
| 105 | // a deadline. |
| 106 | new |
| 107 | TASK_DUE, |
| 108 | "Tasks coming due", |
| 109 | "Before a task's due date, at the lead time you choose.", |
| 110 | "Reminders", |
| 111 | |
| 112 | .with, |
| 113 | ]; |
| 114 | |
| 115 | /// GoingsOn's notification registry. |
| 116 | pub static NOTIFS: Registry = new; |
| 117 | |
| 118 | |
| 119 | |
| 120 | use *; |
| 121 | use Unset; |
| 122 | |
| 123 | |
| 124 | |
| 125 | // Everything a `const` cannot check: duplicate ids, key shape, and a |
| 126 | // choice whose default is not one of its options. |
| 127 | NOTIFS.check.expect; |
| 128 | |
| 129 | |
| 130 | |
| 131 | |
| 132 | // Adoption must not silently stop a notification that fires today. |
| 133 | // Scoped to the three the adoption grandfathered rather than to the |
| 134 | // whole registry, because a new kind ships off and this test used to |
| 135 | // say the opposite. |
| 136 | for id in |
| 137 | assert! |
| 138 | NOTIFS.is_on, |
| 139 | "{id} fired before the registry existed and must ship on" |
| 140 | ; |
| 141 | |
| 142 | |
| 143 | |
| 144 | |
| 145 | |
| 146 | // Max, 2026-08-17: new kinds ship off, and onboarding points at them. |
| 147 | assert!; |
| 148 | |
| 149 | |
| 150 | |
| 151 | |
| 152 | let lead = NOTIFS |
| 153 | .value |
| 154 | .expect; |
| 155 | assert_eq!; |
| 156 | |
| 157 | |
| 158 | |
| 159 | |
| 160 | use CatchUp; |
| 161 | |
| 162 | let quiet: = NOTIFS |
| 163 | .kinds |
| 164 | .iter |
| 165 | .filter |
| 166 | .map |
| 167 | .collect; |
| 168 | assert_eq!; |
| 169 | |
| 170 | |
| 171 | |
| 172 | |
| 173 | // The whole of what keeps `event-reminder.lead` -- if one is ever |
| 174 | // declared -- apart from `event_lead_minutes` in one table. |
| 175 | for generated in NOTIFS.config |
| 176 | assert! |
| 177 | generated.key.contains, |
| 178 | "{} is not dotted and could collide with an app key", |
| 179 | generated.key |
| 180 | ; |
| 181 | |
| 182 | |
| 183 | |
| 184 |