feat(notifications): mentions, inbox, and table watches - #1243
Draft
joepio wants to merge 51 commits into
Draft
Conversation
️✅ There are no secrets present in this pull request anymore.If these secrets were true positive and are still valid, we highly recommend you to revoke them. 🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request. |
cursor
Bot
force-pushed
the
cursor/notifications-design-0fcd
branch
from
August 15, 2026 12:25
9c48bec to
d280d93
Compare
…ions Propose actor-side mention edges, personal NotificationItems, client-side watch matching for collections/tables, and a phased delivery path from in-app center through Tauri/OS notifications to later push-as-wake. Co-authored-by: Joep Meindertsma <joep@ontola.io>
Lock personal-drive sync for read/dismissed, and document the local notification vs APNs/FCM split for Tauri mobile (wake-then-sync payload). Co-authored-by: Joep Meindertsma <joep@ontola.io>
Lock App menu order (User Settings → Notifications → Settings) and split inbox route from App Settings preference section. Co-authored-by: Joep Meindertsma <joep@ontola.io>
Add the notifications ontology, write actor-side `mentions` from TipTap and chat, materialize personal NotificationItems via NotificationEngine, and ship the sidebar inbox plus a Watch toggle on tables. Co-authored-by: Joep Meindertsma <joep@ontola.io>
Add Playwright specs for the notifications sidebar/inbox, unread badge, mark-all-read, and table Watch toggle. Fix WatchToggle busy-state handling to avoid bare try/finally for React Compiler. Co-authored-by: Joep Meindertsma <joep@ontola.io>
Subscribe inbox rows to LocalChange via useValue, notify the store after mark/dismiss saves, and fall back to direct store writes if the engine is still starting. Parent the notifications ontology under https://atomicdata.dev/ for rights walks. All four notifications Playwright specs are green; populate unit asserts the ontology seeds. Co-authored-by: Joep Meindertsma <joep@ontola.io>
Show Web Notification API / tauri-plugin-notification banners when the tab or window is unfocused; toast when focused. Permission is requested from Watch enable and App Settings, never on cold start. Mark-read and dismiss cancel matching OS notifications. Wire the Tauri plugin + capabilities; document remaining Phase 5 push work. Co-authored-by: Joep Meindertsma <joep@ontola.io>
- WatchToggle on Collection views; Settings watches list (mute/remove) - E2E: watch + simulated other-agent child materializes NotificationItem - DevicePushToken ontology + registerDevicePushToken helper - Hub push_wake helpers (wake-only payload) + commit_monitor hook note - Cold-start push tap queue; flushPendingWatches for tests Co-authored-by: Joep Meindertsma <joep@ontola.io>
- data-watching / mark-all-read / notifications-empty test ids - flushPendingWatches after brief wait for async ResourceUpdated - Avoid brittle lingui-scrambled accessible names in assertions Co-authored-by: Joep Meindertsma <joep@ontola.io>
… tap wire - E2E: mention ResourceUpdated (other actor) materializes inbox item - E2E: mark read on A clears badge on B (same agent, two contexts) - Wire pushWakeTap → navigate in NotificationOsPresenter - useDevicePushRegistration on launch; suppress-if-read helpers Co-authored-by: Joep Meindertsma <joep@ontola.io>
- Wait for engine; in-app nav after mention materialization - Second-device sign-in without Continue; wait for agent then open inbox Co-authored-by: Joep Meindertsma <joep@ontola.io>
- NotificationEngine re-runs mention backlog on DriveChanged - E2E: A invites B, mentions B while B's engine stopped, B reconciles - Closes reverse-query + two-agent invite gaps in planning/coverage Co-authored-by: Joep Meindertsma <joep@ontola.io>
Top-bar Share on a freshly UI-created document can leave useCanWrite stuck false so Create Invite never appears. Use the same drive context-menu share path as the authorization invite e2e instead. Co-authored-by: Joep Meindertsma <joep@ontola.io>
Fix Share Create Invite stuck read-only on new DID docs by resetting useCanWrite optimistically on subject/newness and re-checking when resource.new flips after genesis. Phase 5: handlePushWake/processPushWake (fetch → reconcile → suppress-if-read), receive queue wired in NotificationOsPresenter, and commit_monitor mention → wake stub (provider send still TODO). Co-authored-by: Joep Meindertsma <joep@ontola.io>
Pick tauri-plugin-push-notifications for Phase 5 (documented; Cargo gated until Firebase/APNs project files exist). Add tauriPushBridge to drain local notification cold-start taps and optionally fetch a device token. Hub enqueue_push_wakes looks up DevicePushToken rows per wake agent. Co-authored-by: Joep Meindertsma <joep@ontola.io>
Add PushSender trait with LoggingPushSender default (ready for FCM/APNs), watch subscription wake matching (target/parent, mute/enabled, owner), and route commit_monitor through wakes_for_committed_resource. Co-authored-by: Joep Meindertsma <joep@ontola.io>
Add EnvPushSender (ATOMIC_FCM_* / ATOMIC_APNS_* bearers) installed at serve boot, falling back to log-only without secrets. Gate the Tauri push plugin behind --features mobile-push and document enablement in planning/notifications.md. Co-authored-by: Joep Meindertsma <joep@ontola.io>
Co-authored-by: Joep Meindertsma <joep@ontola.io>
Add DirectMessage and AccessRequest ontology classes, actor-side mentions so the existing engine materializes inbox items, a Send message dialog for drive collaborators, Request access on Share and Unauthorized, and Grant from the inbox / request page. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
The notifications page queried WASM once on mount. A second browser signing in as the same agent could miss NotificationItems that arrived via drive sync after that query, so mark-read never had a row to sync. Scan the in-memory store (and re-query on ResourceUpdated / sync status), wait for the seeded item to drain before opening device B, and keep the index query as a backlog supplement. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
pendingDirtyCount can stay non-zero for unrelated in-flight saves. Wait until the NotificationItem is fetchable over HTTP so device B has something to sync. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
The App menu label is often the agent name, so waiting for a User Settings link races sign-in. Agent subject is enough before opening the inbox. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Drive sync can finish before nested NotificationItems are in the parent index, so a second device's WASM collection stays empty. Ask /query for isA=NotificationItem on the personal drive and hydrate those subjects into the store. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Device B's drive sync can omit nested NotificationItems. Fetch the item A already posted so the inbox and mark-read path can run. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Restricting the listener to NotificationItem classes missed items whose isA had not materialized yet at notify time. Debounced rescan of the in-memory store on every ResourceUpdated. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
bootstrap() no-ops on an already-seeded store, so --initialize never added NotificationItem and commits 500'd while fetching the class from atomicdata.dev. Re-run populate on initialize when SHORTNAME exists. Also wait for the seeded inbox item to drain before opening device B. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Silent wakes never reach a killed phone. The hub now sends a generic lock-screen alert (FCM notification + APNs aps.alert) plus the existing about/type data bag, and mints FCM/APNs JWTs from a service-account JSON or .p8 on disk. Tauri Android gets POST_NOTIFICATIONS and an optional google-services hook; Flutter registers firebase_messaging after sign-in onto the same DevicePushToken ontology. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
/app/dev-drive created a single personal workspace, so the Notifications folder appeared as a child of Dev drive. Provision a private Personal drive for inbox/watches and a non-personal Dev drive workspace, then land on the workspace. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Creating a Personal drive plus Dev drive made every before() slower and turned newDrive()'s waitForURL(/did:ad:/) into a no-op (already on a workspace). Skip the unused Personal ontology, wait for the new drive title, and put the workspace in the agent secret's initialDrive so second-context specs still open Dev drive subjects. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
/app/dev-drive now provisions Personal + Dev drive. Under suite-wide load that setup ate the old 60s budget before the spec body ran. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Callers like the user-drives spec open the helper from User Settings, which has no resource subject. Treat the previous subject as optional. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Keep the /app/dev-drive secret's initialDrive on Personal so inbox writes and queries use the same drive. Remount the notification engine when the Agent resource gains personalDrive (invite persist), and recover from a failed first start instead of leaving __notificationEngine unset. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
reloadWatches no longer replaces the watch map with an empty collection index after a just-saved subscription. The unread badge always hits server /query, and invite e2e opens /app/invite on FRONTEND_URL so the invitee uses the same bundle as the rest of the suite. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Re-fetch notification items over the websocket so device B sees mark-read commits. After invite persist, rebuild the Agent with Personal as initialDrive so the engine remounts on the private inbox drive. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Always HTTP-refetch inbox rows so mark-read is not stuck on a cached unread copy. The second context now signs in through signIn() and waits for Connected before opening the inbox. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
visibleNotificationItems no longer skips `new` rows, so a watch or mention upsert appears in the inbox before the collection index catches up. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
cargo fmt --check and pnpm lint (oxlint + oxfmt) are the first CI gates. Format the notifications/push Rust and TS that were blocking the pipeline before e2e could run. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
CI fail-fast passed rustfmt/oxlint; flutter analyze then exited 1 on unused_import / unnecessary_import in the push notification files. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
The bin re-declares modules instead of using the lib. push_provider imports crate::push_credentials, which compiled in the lib but not in atomic-server's binary until the module is listed in bin.rs. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
CI linked a stale rustc incremental binary (old simple.rs line numbers, 10 tests instead of 12) and the suite shares a process-global OnceLock DB. Touch sources so cargo rebuilds, run --test-threads=1, and pin active_drive from setup's returned subject. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Touching only the bridge sources rebuilt simple.rs against a cached atomic_lib rlib that predated DEVICE_PUSH_* url constants (E0425). Clean both packages so cargo rebuilds lib + the bridge together. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Notifications ontology adds access-request (and others) that sort before agent. Assert nested members are sorted by shortname and that Agent is still in the collection. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Dev-drive agents are named Dev User, so signIn's 10s wait for a User Settings row never resolved (and swallowed the timeout). Second contexts then raced presence, meetings, and cold-load. Wait for store.getAgent() instead. Open the inbox via a scrolled, force-clicked sidebar-notifications testid — the App menu sits below two drives. Ignore NotificationEngine isA/mentions /query frames in the refresh-storm regression. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Dev-drive secrets pin initialDrive to Personal. A second context that only opens ?subject=<Dev drive> still has Personal as store.getDrive(), so presence, meetings, and cold-load children miss the workspace. Click Set as current drive after that navigation. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
The Drive page's Set as current drive control often appears after the previous 5s visibility check, so second contexts stayed on Personal. Call store.setDrive and wait until the session drive matches. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Same-agent extra contexts sign in with Personal as initialDrive, then open a workspace canvas/document. Live WS stays on Personal unless we setDrive from the resource's stamped drive property. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Share getOrCreateFolderByLocalId with Drafts/Forks, list inbox subjects once for the route and badge, match watches via Collection.constraintMatches, and drive e2e folder/drive setup through production helpers. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
createdBy lives on server.properties, not core. Passing undefined into Resource.set crashed Loro in the engine-path notification specs. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
cursor
Bot
force-pushed
the
cursor/notifications-design-0fcd
branch
from
August 19, 2026 15:36
f0c9c34 to
7490b98
Compare
e2e format-check failed on an 80-col import. InvitePage no longer assigns agent.initialDrive on the value from useSettings (react/immutability); new-agent invites rebuild from the secret instead. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Production Vite builds look up chrome text by catalog id. Mentions, watches, DMs, and access-request copy was never extracted, so CI rendered empty headings (the inbox h1 had no accessible name). Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Use testids for the notifications heading and send-message dialog so CI does not depend on translated accessible names. Fall back to the personal drive when the invite host is unreadable, and accept either Dev drive or the invitee's home title after accept. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#77 #1157
Adds in-app notifications for @mentions, watched tables/collections, direct messages, and access requests.
When someone mentions you, a watched query changes, a collaborator DMs you, or someone asks for access, the event lands as a
NotificationItemon your Personal drive — not the current workspace. The App-menu bell opens/app/notifications. Read and dismissed state syncs with that drive.This is the product slice of #77 (mentions, subscriptions, inbox — not feeds) and the mention-picker work in #1157 (search agents outside the current drive).
Mentions
@write amentionsedge on the authored resource (actor-side, not inbox spam).NotificationEnginereverse-queries those edges and materializes inbox rows for the mentioned agent.Watches
UPDATE/DESTROYis matched with the sameconstraintMatchesthe Collection view already uses. Membership and content kinds are both supported.Direct messages and access requests
DirectMessageandAccessRequestclasses in the notifications ontology.Where it lives
Inbox, watches, preferences, and
DevicePushTokens sit under{personalDrive}/notifications/(getOrCreateNotificationsFolder)./app/dev-drivenow matches onboarding: a private Personal drive (personal: true) first, then a Dev workspace (personal: false). The agent secret'sinitialDriveis Personal so inbox writes and queries agree, while the UI lands on Dev.Delivery
tauri-plugin-notification+ Notification API).DevicePushTokenlookup stub (wake-to-sync; payload still comes from the store).Tests
@tomic/libvitest covering engine, watches, and helpers.notifications.spec.tsfor inbox UI, mention/watch paths, and engine materialization.Related Issues
#77 #1157
Checklist