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:
Generated
+563
-11
@@ -144,6 +144,30 @@ dependencies = [
|
||||
"winnow 1.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-compat"
|
||||
version = "0.2.5"
|
||||
@@ -157,6 +181,113 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-executor"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"concurrent-queue",
|
||||
"fastrand",
|
||||
"futures-lite",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"parking",
|
||||
"polling",
|
||||
"rustix",
|
||||
"slab",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-lock"
|
||||
version = "3.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-process"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-signal",
|
||||
"async-task",
|
||||
"blocking",
|
||||
"cfg-if",
|
||||
"event-listener",
|
||||
"futures-lite",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-signal"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"atomic-waker",
|
||||
"cfg-if",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"rustix",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-task"
|
||||
version = "4.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atk"
|
||||
version = "0.18.2"
|
||||
@@ -261,6 +392,19 @@ dependencies = [
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blocking"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-task",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"piper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "8.0.4"
|
||||
@@ -513,6 +657,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.2"
|
||||
@@ -925,6 +1078,33 @@ version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||
|
||||
[[package]]
|
||||
name = "endi"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
|
||||
dependencies = [
|
||||
"enumflags2_derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2_derive"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.4"
|
||||
@@ -962,6 +1142,26 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
|
||||
dependencies = [
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener-strategy"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
@@ -1138,6 +1338,19 @@ version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.34"
|
||||
@@ -1549,6 +1762,12 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
@@ -1883,6 +2102,7 @@ dependencies = [
|
||||
"tauri-build",
|
||||
"tauri-plugin-global-shortcut",
|
||||
"tauri-plugin-log",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-updater",
|
||||
]
|
||||
|
||||
@@ -2207,6 +2427,20 @@ version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "mac-notification-sys"
|
||||
version = "0.6.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd604973958ddcc11b561193c0fb96ba146506ef2f231ef2e7c35fd2cbc9beca"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
"time",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever"
|
||||
version = "0.38.0"
|
||||
@@ -2350,6 +2584,20 @@ dependencies = [
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-rust"
|
||||
version = "4.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe430c879247371c47fdedd328ad4e301e6b30b533c5fd5db1c06d75253d0e46"
|
||||
dependencies = [
|
||||
"futures-lite",
|
||||
"log",
|
||||
"mac-notification-sys",
|
||||
"serde",
|
||||
"tauri-winrt-notification",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.2"
|
||||
@@ -2669,6 +2917,16 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "osakit"
|
||||
version = "0.3.1"
|
||||
@@ -2708,6 +2966,12 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
@@ -2796,6 +3060,17 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "piper"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"fastrand",
|
||||
"futures-io",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.34"
|
||||
@@ -2847,6 +3122,20 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"hermit-abi",
|
||||
"pin-project-lite",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.15.0"
|
||||
@@ -2877,6 +3166,15 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "precomputed-hash"
|
||||
version = "0.1.1"
|
||||
@@ -2975,6 +3273,35 @@ version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-handle"
|
||||
version = "0.6.2"
|
||||
@@ -3601,6 +3928,16 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.10"
|
||||
@@ -3863,7 +4200,7 @@ dependencies = [
|
||||
"tao-macros",
|
||||
"unicode-segmentation",
|
||||
"url",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
"windows-version",
|
||||
"x11-dl",
|
||||
@@ -3945,7 +4282,7 @@ dependencies = [
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"window-vibrancy",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4062,6 +4399,25 @@ dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-notification"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad2fd40946aef810c4be9fd33a2d1b9b397cb79042b2d21c81a0a8f204354fd1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"notify-rust",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.20",
|
||||
"time",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.10.1"
|
||||
@@ -4117,7 +4473,7 @@ dependencies = [
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4142,7 +4498,7 @@ dependencies = [
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"wry",
|
||||
]
|
||||
|
||||
@@ -4195,6 +4551,17 @@ dependencies = [
|
||||
"toml 1.1.4+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-winrt-notification"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f37a6c354fd28fc9e322ed9bd47e3959576dad28c9d58ea1cf888cce1c7ccb36"
|
||||
dependencies = [
|
||||
"thiserror 2.0.20",
|
||||
"windows 0.62.2",
|
||||
"windows-version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
@@ -4542,9 +4909,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
@@ -4594,6 +4973,17 @@ version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-char-property"
|
||||
version = "0.9.0"
|
||||
@@ -5041,7 +5431,7 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a"
|
||||
dependencies = [
|
||||
"webview2-com-macros",
|
||||
"webview2-com-sys",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
@@ -5065,7 +5455,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
|
||||
dependencies = [
|
||||
"thiserror 2.0.20",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
@@ -5130,11 +5520,23 @@ version = "0.61.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-collections 0.2.0",
|
||||
"windows-core 0.61.2",
|
||||
"windows-future",
|
||||
"windows-future 0.2.1",
|
||||
"windows-link 0.1.3",
|
||||
"windows-numerics",
|
||||
"windows-numerics 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections 0.3.2",
|
||||
"windows-core 0.62.2",
|
||||
"windows-future 0.3.2",
|
||||
"windows-numerics 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5146,6 +5548,15 @@ dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.2"
|
||||
@@ -5180,7 +5591,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
"windows-link 0.1.3",
|
||||
"windows-threading",
|
||||
"windows-threading 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core 0.62.2",
|
||||
"windows-link 0.2.1",
|
||||
"windows-threading 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5227,6 +5649,16 @@ dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core 0.62.2",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
@@ -5365,6 +5797,15 @@ dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-version"
|
||||
version = "0.1.7"
|
||||
@@ -5596,7 +6037,7 @@ dependencies = [
|
||||
"webkit2gtk",
|
||||
"webkit2gtk-sys",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
"windows-version",
|
||||
"x11-dl",
|
||||
@@ -5679,6 +6120,76 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "5.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907"
|
||||
dependencies = [
|
||||
"async-broadcast",
|
||||
"async-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-process",
|
||||
"async-recursion",
|
||||
"async-task",
|
||||
"async-trait",
|
||||
"blocking",
|
||||
"enumflags2",
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
"futures-lite",
|
||||
"hex",
|
||||
"libc",
|
||||
"ordered-stream",
|
||||
"rustix",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"tracing",
|
||||
"uds_windows",
|
||||
"uuid",
|
||||
"windows-sys 0.61.2",
|
||||
"winnow 1.0.4",
|
||||
"zbus_macros",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_macros"
|
||||
version = "5.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_names"
|
||||
version = "4.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"winnow 1.0.4",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zcheapstr"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.56"
|
||||
@@ -5777,3 +6288,44 @@ name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "5.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147"
|
||||
dependencies = [
|
||||
"endi",
|
||||
"enumflags2",
|
||||
"serde",
|
||||
"winnow 1.0.4",
|
||||
"zcheapstr",
|
||||
"zvariant_derive",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_derive"
|
||||
version = "5.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_utils"
|
||||
version = "4.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn 3.0.3",
|
||||
"winnow 1.0.4",
|
||||
]
|
||||
|
||||
@@ -95,6 +95,19 @@ pub struct TitleEntry {
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
/// A reminder that has come due, as the desktop's reminder worker needs it.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct DueReminder {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
/// `remind_at` as stored. It names the occurrence: completing or snoozing a
|
||||
/// reminder changes it, and that is how an announcer tells a new occurrence
|
||||
/// from one it has already announced.
|
||||
pub remind_at: String,
|
||||
/// The same instant, in milliseconds since the epoch.
|
||||
pub due_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SavedFilter {
|
||||
pub id: String,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -69,3 +69,9 @@ tauri-plugin-updater = "2"
|
||||
# concept of a global shortcut, and its half of this feature is a share-sheet
|
||||
# intent filter instead.
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
|
||||
# System notifications for due reminders (#5171), raised from the reminder worker in
|
||||
# Rust. The page's own `Notification` goes nowhere in WebKitGTK, and the page's
|
||||
# timer only runs while its window does. Used from Rust only, so no capability
|
||||
# grants it to the webview.
|
||||
tauri-plugin-notification = "2"
|
||||
|
||||
@@ -14,6 +14,7 @@ mod capture;
|
||||
mod commands;
|
||||
mod crossover;
|
||||
mod integration;
|
||||
mod reminders;
|
||||
mod update;
|
||||
|
||||
/// The build a PERSON reads, baked in by the desktop lane at compile time.
|
||||
@@ -87,6 +88,8 @@ pub fn run() {
|
||||
// `setup`, once the store is open and can be asked which one to use — the
|
||||
// plugin only has to exist before then.
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||
// System notifications for due reminders, raised by the `reminders` worker.
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
// Attachment bytes are served to the webview from the local blob store
|
||||
// (M10.7f). Registered on the BUILDER because a scheme has to exist before
|
||||
// the webview is created; the directory it reads from arrives later, in
|
||||
@@ -146,6 +149,9 @@ pub fn run() {
|
||||
// Last: the worker reads the store and the blob store, both managed now.
|
||||
// Its first cycle is the launch sync.
|
||||
autosync::start(app.handle())?;
|
||||
// Announces due reminders from the store, including while the window is
|
||||
// minimised or covered.
|
||||
reminders::start(app.handle())?;
|
||||
// Coming back to the window is when someone is about to look, so it asks
|
||||
// for a cycle. Rate-limited in autosync, because focus flaps constantly.
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
//! Reminders reach you when the window isn't in front (#5171).
|
||||
//!
|
||||
//! The shared frontend's reminder check is a 45-second timer in the page, which
|
||||
//! raises a toast and, if allowed, a page `Notification`. Inside this app that was
|
||||
//! not enough. The page's timer only does anything while the webview is running
|
||||
//! it, and WebKitGTK's page notifications go nowhere. So a reminder that came due
|
||||
//! while Inkwell sat minimised or behind another window was never announced.
|
||||
//!
|
||||
//! This worker owns announcing on the desktop instead. Every [`TICK`] it asks the
|
||||
//! local store which reminders have come due, and announces each occurrence once:
|
||||
//!
|
||||
//! - **always** to the main window, as [`DUE_EVENT`], which the page turns into its
|
||||
//! toast with an Open button;
|
||||
//! - **and as a system notification** when the main window isn't the focused one,
|
||||
//! because a toast in a window nobody is looking at is not an announcement.
|
||||
//!
|
||||
//! The store is the source, not the server, so this works unlinked and offline, and
|
||||
//! a reminder set on the phone announces here once a sync brings it in.
|
||||
//!
|
||||
//! Clicking the system notification doesn't open the note: the notification plugin
|
||||
//! reports clicks only on mobile. The toast waiting in the window does.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use tauri_plugin_notification::NotificationExt;
|
||||
|
||||
use inkwell_core::local::models::DueReminder;
|
||||
use inkwell_core::local::{store, Db};
|
||||
|
||||
/// How often the worker asks the store what has come due. The first look waits one
|
||||
/// tick too, so the page is listening before anything is sent to it.
|
||||
const TICK: Duration = Duration::from_secs(15);
|
||||
/// At launch, only reminders due this recently are announced. Anything older came
|
||||
/// due while the app was closed, and a burst of stale alerts at every start would
|
||||
/// be noise. It still shows on the Reminders page.
|
||||
const RECENT_MS: i64 = 15 * 60 * 1000;
|
||||
|
||||
/// Emitted to the main window for each reminder announced, with a [`DueReminder`].
|
||||
pub const DUE_EVENT: &str = "inkwell://reminder-due";
|
||||
|
||||
/// Start the worker. Called from `setup` once the store is managed.
|
||||
pub fn start(app: &AppHandle) -> std::io::Result<()> {
|
||||
let handle = app.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("reminders".into())
|
||||
.spawn(move || worker(handle))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn worker(app: AppHandle) {
|
||||
let mut announcer = Announcer::default();
|
||||
loop {
|
||||
std::thread::sleep(TICK);
|
||||
let now = now_ms();
|
||||
let due = match due(&app, now) {
|
||||
Ok(due) => due,
|
||||
Err(e) => {
|
||||
log::warn!("could not read due reminders: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
for reminder in announcer.take(due, now) {
|
||||
announce(&app, &reminder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn due(app: &AppHandle, now: i64) -> Result<Vec<DueReminder>, String> {
|
||||
let db = app.state::<Db>();
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
store::due_reminders(&conn, now).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn announce(app: &AppHandle, reminder: &DueReminder) {
|
||||
if let Err(e) = app.emit_to("main", DUE_EVENT, reminder) {
|
||||
log::warn!("could not tell the window about a due reminder: {e}");
|
||||
}
|
||||
if main_window_in_front(app) {
|
||||
return;
|
||||
}
|
||||
let body = if reminder.title.is_empty() {
|
||||
"Reminder"
|
||||
} else {
|
||||
reminder.title.as_str()
|
||||
};
|
||||
if let Err(e) = app
|
||||
.notification()
|
||||
.builder()
|
||||
.title("Inkwell reminder")
|
||||
.body(body)
|
||||
.show()
|
||||
{
|
||||
log::warn!("could not show a reminder notification: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether someone is looking at the main window right now. Unknown counts as no:
|
||||
/// a duplicate alert is a smaller failure than a missed one.
|
||||
fn main_window_in_front(app: &AppHandle) -> bool {
|
||||
app.get_webview_window("main")
|
||||
.is_some_and(|w| w.is_focused().unwrap_or(false) && !w.is_minimized().unwrap_or(true))
|
||||
}
|
||||
|
||||
fn now_ms() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_or(0, |d| d.as_millis() as i64)
|
||||
}
|
||||
|
||||
/// Which occurrences have been announced. Keyed by note id, holding the `remind_at`
|
||||
/// announced: completing or snoozing a reminder changes that value, so the next
|
||||
/// occurrence is new to it.
|
||||
#[derive(Default)]
|
||||
struct Announcer {
|
||||
announced: HashMap<String, String>,
|
||||
primed: bool,
|
||||
}
|
||||
|
||||
impl Announcer {
|
||||
/// The reminders in `due` to announce now. Each occurrence comes back once; on
|
||||
/// the first call, ones due more than [`RECENT_MS`] ago are marked without
|
||||
/// being returned.
|
||||
fn take(&mut self, due: Vec<DueReminder>, now: i64) -> Vec<DueReminder> {
|
||||
// Forget notes that are no longer due (completed, snoozed, trashed), so the
|
||||
// map stays the size of the due list.
|
||||
self.announced
|
||||
.retain(|id, _| due.iter().any(|d| d.id == *id));
|
||||
let mut fresh = Vec::new();
|
||||
for reminder in due {
|
||||
if self.announced.get(&reminder.id) == Some(&reminder.remind_at) {
|
||||
continue;
|
||||
}
|
||||
self.announced
|
||||
.insert(reminder.id.clone(), reminder.remind_at.clone());
|
||||
if !self.primed && now - reminder.due_ms > RECENT_MS {
|
||||
continue;
|
||||
}
|
||||
fresh.push(reminder);
|
||||
}
|
||||
self.primed = true;
|
||||
fresh
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const NOW: i64 = 1_800_000_000_000;
|
||||
|
||||
fn due(id: &str, remind_at: &str, ago_ms: i64) -> DueReminder {
|
||||
DueReminder {
|
||||
id: id.into(),
|
||||
title: id.into(),
|
||||
remind_at: remind_at.into(),
|
||||
due_ms: NOW - ago_ms,
|
||||
}
|
||||
}
|
||||
|
||||
fn ids(list: &[DueReminder]) -> Vec<&str> {
|
||||
list.iter().map(|d| d.id.as_str()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_occurrence_is_announced_once() {
|
||||
let mut a = Announcer::default();
|
||||
let first = a.take(vec![due("a", "t1", 1_000)], NOW);
|
||||
assert_eq!(ids(&first), ["a"]);
|
||||
assert!(a
|
||||
.take(vec![due("a", "t1", 16_000)], NOW + 15_000)
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_launch_a_stale_backlog_is_marked_not_announced() {
|
||||
let mut a = Announcer::default();
|
||||
let first = a.take(
|
||||
vec![due("old", "t0", RECENT_MS + 1), due("new", "t1", 60_000)],
|
||||
NOW,
|
||||
);
|
||||
assert_eq!(ids(&first), ["new"]);
|
||||
assert!(a
|
||||
.take(vec![due("old", "t0", RECENT_MS + 15_001)], NOW + 15_000)
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn after_launch_anything_newly_due_is_announced_however_late() {
|
||||
let mut a = Announcer::default();
|
||||
a.take(Vec::new(), NOW);
|
||||
// Pulled in by a sync, already an hour overdue.
|
||||
let later = a.take(vec![due("synced", "t1", 3_600_000)], NOW);
|
||||
assert_eq!(ids(&later), ["synced"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_new_occurrence_of_the_same_note_is_announced_again() {
|
||||
let mut a = Announcer::default();
|
||||
a.take(vec![due("daily", "mon", 1_000)], NOW);
|
||||
// Completed: it leaves the due list until the next occurrence comes due.
|
||||
a.take(Vec::new(), NOW + 15_000);
|
||||
let next = a.take(vec![due("daily", "tue", 1_000)], NOW + 86_400_000);
|
||||
assert_eq!(ids(&next), ["daily"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_snoozed_reminder_that_is_due_again_is_announced_again() {
|
||||
let mut a = Announcer::default();
|
||||
a.take(vec![due("n", "09:00", 1_000)], NOW);
|
||||
// Snoozed and due again before a tick saw it leave the list.
|
||||
let again = a.take(vec![due("n", "09:10", 1_000)], NOW + 600_000);
|
||||
assert_eq!(ids(&again), ["n"]);
|
||||
}
|
||||
}
|
||||
@@ -295,3 +295,27 @@ export async function onCaptured(handler: () => void): Promise<() => void> {
|
||||
if (!events) return () => {};
|
||||
return events.listen(CAPTURED_EVENT, () => handler());
|
||||
}
|
||||
|
||||
/** A reminder the desktop's reminder worker found due (desktop/src-tauri/src/reminders.rs). */
|
||||
export interface DueReminder {
|
||||
id: string;
|
||||
title: string;
|
||||
remind_at: string;
|
||||
due_ms: number;
|
||||
}
|
||||
|
||||
/** Fired at the main window for each reminder occurrence, once. */
|
||||
const REMINDER_DUE_EVENT = "inkwell://reminder-due";
|
||||
|
||||
/**
|
||||
* Run `handler` for each reminder the desktop announces. On the desktop the Rust
|
||||
* worker decides what is due, and raises the system notification itself when the
|
||||
* window isn't in front; the page only shows its toast.
|
||||
*
|
||||
* Returns an unlisten function, or a no-op where there is no event API.
|
||||
*/
|
||||
export async function onReminderDue(handler: (reminder: DueReminder) => void): Promise<() => void> {
|
||||
const events = window.__TAURI__?.event;
|
||||
if (!events) return () => {};
|
||||
return events.listen<DueReminder>(REMINDER_DUE_EVENT, (e) => handler(e.payload));
|
||||
}
|
||||
|
||||
@@ -1,23 +1,33 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { repo } from "../adapters";
|
||||
import { isDesktop, onReminderDue } from "../desktop/bridge";
|
||||
import router from "../router";
|
||||
import { useUiStore } from "./ui";
|
||||
import type { Note } from "./notes";
|
||||
|
||||
// Foreground reminder delivery — NO Web Push / service worker (per operator). While
|
||||
// the app is open, poll for due reminders and surface each once as a toast (and, if
|
||||
// the user opted in, a page-context OS notification). Real background delivery is a
|
||||
// native-client concern.
|
||||
// Reminder delivery. In a browser it is foreground only — NO Web Push / service
|
||||
// worker (per operator): while the tab is open, poll for due reminders and surface
|
||||
// each once as a toast (and, if the user opted in, a page-context OS notification).
|
||||
//
|
||||
// The desktop doesn't poll here. Its Rust worker reads the local store, decides
|
||||
// what is due, raises the system notification itself when the window isn't in
|
||||
// front, and tells this page so it can show the toast (#5171). The page's
|
||||
// `Notification` went nowhere in WebKitGTK, and its timer stopped with the window.
|
||||
const POLL_MS = 45_000;
|
||||
const RECENT_MS = 15 * 60 * 1000; // on first load, only announce reminders due this recently
|
||||
|
||||
export const useReminderStore = defineStore("reminders", () => {
|
||||
const osSupported = typeof Notification !== "undefined";
|
||||
const desktop = isDesktop();
|
||||
// The desktop raises its own system notifications; there is nothing to opt into.
|
||||
const osSupported = !desktop && typeof Notification !== "undefined";
|
||||
const osEnabled = ref(osSupported && Notification.permission === "granted");
|
||||
// note id → the remind_at we already fired, so each occurrence notifies at most once.
|
||||
const notified = new Map<string, string>();
|
||||
let timer: ReturnType<typeof setInterval> | undefined;
|
||||
// The desktop's subscription to its reminder worker, held as the promise so stop()
|
||||
// can undo one whose registration hasn't finished yet.
|
||||
let listening: Promise<() => void> | undefined;
|
||||
let primed = false;
|
||||
|
||||
// Single owner of the reminders endpoint — the RemindersView reads its list through
|
||||
@@ -26,8 +36,17 @@ export const useReminderStore = defineStore("reminders", () => {
|
||||
return repo.notes.reminders();
|
||||
}
|
||||
|
||||
/** Show the in-app toast for a due reminder; returns the title it showed. */
|
||||
function toast(id: string, displayTitle: string): string {
|
||||
const title = displayTitle || "Reminder";
|
||||
useUiStore().showToast(`⏰ ${title}`, {
|
||||
label: "Open",
|
||||
run: () => void router.push({ path: "/", query: { open: id } }),
|
||||
});
|
||||
return title;
|
||||
}
|
||||
|
||||
async function check(): Promise<void> {
|
||||
const ui = useUiStore();
|
||||
let notes: Note[];
|
||||
try {
|
||||
notes = await fetchReminders();
|
||||
@@ -42,11 +61,7 @@ export const useReminderStore = defineStore("reminders", () => {
|
||||
notified.set(n.id, n.remind_at);
|
||||
// Silently prime a stale backlog on first load; only announce recently-due ones.
|
||||
if (!primed && now - due > RECENT_MS) continue;
|
||||
const title = n.display_title || "Reminder";
|
||||
ui.showToast(`⏰ ${title}`, {
|
||||
label: "Open",
|
||||
run: () => void router.push({ path: "/", query: { open: n.id } }),
|
||||
});
|
||||
const title = toast(n.id, n.display_title);
|
||||
if (osEnabled.value) {
|
||||
try {
|
||||
new Notification("Inkwell reminder", { body: title });
|
||||
@@ -59,6 +74,10 @@ export const useReminderStore = defineStore("reminders", () => {
|
||||
}
|
||||
|
||||
function start(): void {
|
||||
if (desktop) {
|
||||
if (!listening) listening = onReminderDue((r) => void toast(r.id, r.title));
|
||||
return;
|
||||
}
|
||||
if (timer) return;
|
||||
void check();
|
||||
timer = setInterval(() => void check(), POLL_MS);
|
||||
@@ -66,6 +85,8 @@ export const useReminderStore = defineStore("reminders", () => {
|
||||
function stop(): void {
|
||||
if (timer) clearInterval(timer);
|
||||
timer = undefined;
|
||||
void listening?.then((off) => off());
|
||||
listening = undefined;
|
||||
}
|
||||
async function enableOs(): Promise<void> {
|
||||
if (!osSupported) return;
|
||||
|
||||
@@ -7,9 +7,16 @@ import AsyncState from "../components/AsyncState.vue";
|
||||
import EmptyState from "../components/EmptyState.vue";
|
||||
import NoteEditor from "../components/NoteEditor.vue";
|
||||
import NoteGrid from "../components/NoteGrid.vue";
|
||||
import { isDesktop } from "../desktop/bridge";
|
||||
|
||||
const reminders = useReminderStore();
|
||||
|
||||
// What this surface actually does with a due reminder. The desktop's worker raises
|
||||
// system notifications while it runs; a browser tab can only alert while it is open.
|
||||
const deliveryNote = isDesktop()
|
||||
? "Due reminders show as system notifications while Inkwell is running, even minimised."
|
||||
: "Due reminders pop up while this tab is open. The desktop and Android apps alert you in the background.";
|
||||
|
||||
const { items, loading, error, load } = useNoteList(() => reminders.fetchReminders(), "Couldn't load reminders.");
|
||||
|
||||
const { editing, open: openEditor, close: closeEditor, navigate: onNavigate } = useNoteEditor({
|
||||
@@ -37,9 +44,7 @@ onMounted(load);
|
||||
</button>
|
||||
<span v-else-if="reminders.osEnabled" class="text-xs text-neutral-400">Notifications on ✓</span>
|
||||
</div>
|
||||
<p class="mb-5 text-xs text-neutral-400">
|
||||
Due reminders pop up while Inkwell is open. Background alerts arrive with the desktop & mobile apps.
|
||||
</p>
|
||||
<p class="mb-5 text-xs text-neutral-400">{{ deliveryNote }}</p>
|
||||
|
||||
<AsyncState :loading="loading" :error="error || undefined" error-title="Couldn't load reminders" @retry="load">
|
||||
<EmptyState
|
||||
|
||||
Reference in New Issue
Block a user