Skip to content

feat(useNotifications): notification lifecycle management with adapters#146

Open
johnleider wants to merge 49 commits intomasterfrom
feat/notifications-plugin
Open

feat(useNotifications): notification lifecycle management with adapters#146
johnleider wants to merge 49 commits intomasterfrom
feat/notifications-plugin

Conversation

@johnleider
Copy link
Member

Summary

  • useNotifications composable wrapping createQueue with notification-specific lifecycle (read/seen/archive/snooze timestamps, bulk ops, reactive counters)
  • Plugin triple via createPluginContextcreateNotificationsPlugin, createNotificationsContext, useNotifications
  • Built-in adapters for FCM, OneSignal, and Knock via @vuetify/v0/notifications subpath
  • Documentation page at composables/plugins/use-notifications

What's included

Area Files
Core composable packages/0/src/composables/useNotifications/index.ts
Tests (18) packages/0/src/composables/useNotifications/index.test.ts
FCM adapter packages/0/src/composables/useNotifications/adapters/fcm.ts
OneSignal adapter packages/0/src/composables/useNotifications/adapters/onesignal.ts
Knock adapter packages/0/src/composables/useNotifications/adapters/knock.ts
Subpath export packages/0/src/notifications/index.ts + package.json
Docs apps/docs/src/pages/composables/plugins/use-notifications.md

API

// Plugin installation
app.use(createNotificationsPlugin({ adapter: createFcmAdapter(messaging) }))

// Component usage
const notifications = useNotifications()
const ticket = notifications.notify({ subject: 'Saved', severity: 'success', timeout: 3000 })

// State mutations (timestamps, not booleans)
notifications.read(id)
notifications.archive(id)
notifications.readAll()

// Reactive state
notifications.items        // ShallowRef<NotificationTicket[]>
notifications.unreadCount  // ComputedRef<number>

Test plan

  • 18 unit tests: core lifecycle, state mutations, events, adapter integration
  • 3023 full suite — no regressions
  • Typecheck clean
  • Lint clean
  • knip + sherif clean
  • Manual: verify docs page renders with mermaid diagrams and code groups
  • Manual: verify DocsApi generates correct API reference

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 5, 2026

Open in StackBlitz

commit: 70b8573

@johnleider johnleider force-pushed the feat/notifications-plugin branch 15 times, most recently from a54eb68 to 50d5acc Compare March 12, 2026 00:18
- Types: NotificationInput, NotificationTicket, NotificationsContext
- Core: createNotifications wrapping createQueue with lifecycle state
  mutations (read/seen/archive/snooze), bulk ops, reactive counts
- Plugin: createNotificationsContext, createNotificationsPlugin, useNotifications
- Adapter: event-driven integration hook for external services
- Barrel export from composables/index.ts
Core lifecycle, state mutations, events, and adapter integration.
Built-in adapters for popular notification services:
- createFcmAdapter: Firebase Cloud Messaging (inbound)
- createOneSignalAdapter: OneSignal Web SDK (inbound)
- createKnockAdapter: Knock feeds (inbound + outbound)

Available via `@vuetify/v0/notifications` subpath export.
primaryAction/secondaryAction are component-level concerns.
Use the data bag for custom payloads instead.
Aligns with plugin convention — useTheme, useLogger, useFeatures, etc.
- Add fallback to createPluginContext (useNotifications works without provider)
- Remove double onScopeDispose (queue already self-disposes)
- Fix stale "actions" references in JSDoc and docs
- Fix mermaid diagram arrow direction
- Fix dismiss(id) → unregister(id) in docs
- Remove redundant type assertions
- Add @see URL to @module JSDoc
- unreadItems: notifications without readAt
- archivedItems: notifications with archivedAt
- snoozedItems: notifications with snoozedUntil

unreadCount now derives from unreadItems.length for efficiency.
- Adapter: interface with setup/dispose, wired in plugin setup callback
  with app.onUnmount cleanup (matches useFeatures/useTheme pattern)
- Adapter removed from NotificationsOptions, lives on NotificationsPluginOptions
- createNotifications() is now adapter-agnostic
- Bulk ops (readAll/archiveAll) wrapped in queue.batch()
- Updated all 3 adapters (FCM, OneSignal, Knock) to implement interface
- Manual sync pattern confirmed correct (reactive:true doesn't help)
Interactive example demonstrating derived collections, ticket
convenience methods, seen/read distinction, bulk operations,
and state lifecycle diagram.
Eliminates shallowRef + triggerRef + event subscriptions in favor of
useProxyRegistry which handles reactive state via shallowReactive.
Derived collections (unreadItems, archivedItems, snoozedItems) now
compute from proxy.values instead of items.value.
Bulk ops (readAll/archiveAll) now emit per-item domain events so
adapters like Knock correctly sync state. Fixes type safety issues,
adapter memory leaks, and aligns fallback with established patterns.
…use useProxyRegistry

Also wires up useNotifications in docs app as a local message bus:
- Skillz store publishes notification on pending tour
- SkillzResume.vue consumes via useProxyRegistry filtered by data.type
@johnleider johnleider force-pushed the feat/notifications-plugin branch from aede424 to 7c0f4e6 Compare March 13, 2026 17:02
… SSR safety

Replace manual createNotificationsContext/createNotificationsPlugin/useNotifications
with createPluginContext pattern matching useLogger, useRules, useRtl. Complete the
fallback stub with all RegistryContext methods. Add IN_BROWSER guard in Knock adapter
to prevent server-side WebSocket/HTTP connections during SSR.
notifications.notify() was tautological. send() reads more naturally
and aligns with messaging API conventions.
@johnleider johnleider force-pushed the master branch 5 times, most recently from a6133f0 to eb8ed41 Compare March 17, 2026 02:24
… 1 notification

Linter collapsed separate send() calls into multi-arg form.
send() takes 1 arg — extra args were silently ignored.
Registry stores full notification lifecycle (permanent record).
Queue manages display surface (toasts) with FIFO ordering and auto-dismiss.

- send() registers in both registry and queue
- onboard() registers in registry only (bulk load, no toast)
- ticket.dismiss() removes from queue only (toast gone, stays in inbox)
- ticket.unregister() removes from both (full removal)
- Queue auto-dismiss preserves registry entry
- Configurable timeout per-notification or via options default (3000ms)
- Fix "built on createQueue" to describe dual registry+queue architecture
- Fix architecture diagram (Registry+Queue→Core, not Registry→Queue→Core)
- Replace fabricated reactivity table with actual API surface
- Remove stale Firebase/OneSignal references
- Remove fabricated timeout TIP (timeout is now a real field)
- Add create-registry to related links
Bridges @novu/js with useNotifications. Handles inbound (initial list +
real-time notification_received event) and outbound (read, unread, seen,
archive, unarchive). Maps Novu severity levels to NotificationSeverity.
Accept optional severity() function in NovuAdapterOptions.
Falls back to default critical/high→error, medium→warning, low→info.
- Replace manual toast timers (watch+setTimeout) with queue auto-dismiss
- Use Snackbar.* components for toast rendering with TransitionGroup
- Fix proxy: use useProxyRegistry instead of nonexistent notifications.proxy
- Add timeout per scenario type (-1 for persistent, 4000 for toasts)
…ration

- SnackbarPortal: remove ticket.select() to prevent scrim on mount,
  use ticket.unregister() on unmount, add inheritAttrs:false so class
  binds to the inner Atom rather than the Teleport root
- basic.vue: rewrite example with stacked card UI — newest toast front,
  older toasts peek behind with scale/inset offset, hover expands all
  with smooth transitions, queue pause/resume on hover, leave delay
  prevents collapse while mouse moves to upper items
- NotificationProvider.vue: use Snackbar.Portal with fixed positioning,
  drive toasts from notifications.queue via useProxyRegistry
…ean itemStyle

- SnackbarRoot: use useId() as prop default (stable) instead of inside toRef getter
- NotificationProvider example: remove stale :severity prop (no longer on SnackbarRoot)
- queue.vue example: remove unused _total param from itemStyle
- index.test.ts: fix scoped slot render syntax in integration test
… Novu tests, docs fixes

- Add app.onUnmount for context/queue disposal before adapter guard
- Add tests for 6 untested events (unread, seen, archived, unarchived, snoozed, unsnoozed)
- Add Novu adapter test suite (8 tests)
- Strengthen persistent timeout test to verify timer survival
- Document Snackbar.Close queue removal behavior
- Replace inline inject type with SnackbarQueueContext
- Remove empty SnackbarCloseSlotProps interface
- Add JSDoc to NotificationTicket.dismiss
- Simplify Knock adapter dispose with single ctx guard
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant