desktop: reminders reach you when the window isn't in front
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Successful in 11s
CI & Build / integration (push) Successful in 36s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m30s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m38s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m38s
Desktop (Tauri) / Update manifest (push) Successful in 5s
Android / Kotlin + Rust (APK) (push) Successful in 8m45s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Successful in 11s
CI & Build / integration (push) Successful in 36s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m30s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m38s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m38s
Desktop (Tauri) / Update manifest (push) Successful in 5s
Android / Kotlin + Rust (APK) (push) Successful in 8m45s
A Rust worker reads due reminders from the local store every 15 s and announces each occurrence once: always to the main window as a toast, and as a system notification (tauri-plugin-notification) when that window isn't focused. The page no longer polls on the desktop; its Notification went nowhere in WebKitGTK and its timer stopped with the window. The Reminders page says what each surface actually does. Core gains store::due_reminders, compared by instant, not by string. Refs #5171 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -375,6 +375,41 @@ pub fn reminders(conn: &Connection) -> rusqlite::Result<Vec<Note>> {
|
||||
ids.iter().map(|id| load_note(conn, id)).collect()
|
||||
}
|
||||
|
||||
/// Reminders due at or before `now_ms`, soonest first, trash excluded.
|
||||
///
|
||||
/// Read on a timer by the desktop's reminder worker, so only the notes that are
|
||||
/// actually due are loaded in full (for their title). A `remind_at` that doesn't
|
||||
/// parse is left out: it can never come due, and announcing it would be a guess.
|
||||
pub fn due_reminders(conn: &Connection, now_ms: i64) -> rusqlite::Result<Vec<DueReminder>> {
|
||||
let set: Vec<(String, String)> = {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, remind_at FROM notes WHERE trashed = 0 AND remind_at IS NOT NULL ORDER BY remind_at ASC",
|
||||
)?;
|
||||
let rows = stmt.query_map([], |r| Ok((r.get(0)?, r.get(1)?)))?;
|
||||
rows.collect::<rusqlite::Result<_>>()?
|
||||
};
|
||||
let mut due = Vec::new();
|
||||
for (id, remind_at) in set {
|
||||
let Ok(at) = DateTime::parse_from_rfc3339(&remind_at) else {
|
||||
continue;
|
||||
};
|
||||
let due_ms = at.timestamp_millis();
|
||||
if due_ms > now_ms {
|
||||
continue;
|
||||
}
|
||||
let title = load_note(conn, &id)?.display_title;
|
||||
due.push(DueReminder {
|
||||
id,
|
||||
title,
|
||||
remind_at,
|
||||
due_ms,
|
||||
});
|
||||
}
|
||||
// By instant, not by string: a synced `+00:00` and a local `Z` sort apart.
|
||||
due.sort_by_key(|d| d.due_ms);
|
||||
Ok(due)
|
||||
}
|
||||
|
||||
pub fn titles(conn: &Connection) -> rusqlite::Result<Vec<TitleEntry>> {
|
||||
// Names come from `load_note` so the palette and the card can never disagree
|
||||
// about what a note is called. The command palette reads this; correctness
|
||||
@@ -1521,6 +1556,45 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_reminders_already_due_are_due() {
|
||||
let conn = db();
|
||||
let past = note(&conn, "past");
|
||||
let future = note(&conn, "future");
|
||||
let binned = note(&conn, "binned");
|
||||
update_note(
|
||||
&conn,
|
||||
&past.id,
|
||||
&json!({"remind_at": "2026-03-01T09:00:00.000Z"}),
|
||||
)
|
||||
.expect("remind");
|
||||
update_note(
|
||||
&conn,
|
||||
&future.id,
|
||||
&json!({"remind_at": "2026-03-01T11:00:00.000Z"}),
|
||||
)
|
||||
.expect("remind");
|
||||
update_note(
|
||||
&conn,
|
||||
&binned.id,
|
||||
&json!({"remind_at": "2026-03-01T08:00:00.000Z"}),
|
||||
)
|
||||
.expect("remind");
|
||||
trash(&conn, &binned.id).expect("trash");
|
||||
let ten = DateTime::parse_from_rfc3339("2026-03-01T10:00:00Z")
|
||||
.expect("rfc3339")
|
||||
.timestamp_millis();
|
||||
|
||||
let due = due_reminders(&conn, ten).expect("due");
|
||||
assert_eq!(due.len(), 1);
|
||||
assert_eq!(due[0].id, past.id);
|
||||
assert_eq!(due[0].title, past.display_title);
|
||||
assert_eq!(due[0].remind_at, "2026-03-01T09:00:00.000Z");
|
||||
assert_eq!(due[0].due_ms, ten - 3_600_000);
|
||||
// At the instant itself it is due.
|
||||
assert_eq!(due_reminders(&conn, ten + 3_600_000).expect("due").len(), 2);
|
||||
}
|
||||
|
||||
// ---- labels ----------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user