payload-notifications 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +164 -0
  2. package/dist/api.d.ts +11 -0
  3. package/dist/api.d.ts.map +1 -0
  4. package/dist/api.js +144 -0
  5. package/dist/api.js.map +1 -0
  6. package/dist/components/NotificationBell.d.ts +4 -0
  7. package/dist/components/NotificationBell.d.ts.map +1 -0
  8. package/dist/components/NotificationBell.js +318 -0
  9. package/dist/components/NotificationBell.js.map +1 -0
  10. package/dist/components/NotificationBell.module.css +184 -0
  11. package/dist/components/NotificationItem.d.ts +11 -0
  12. package/dist/components/NotificationItem.d.ts.map +1 -0
  13. package/dist/components/NotificationItem.js +97 -0
  14. package/dist/components/NotificationItem.js.map +1 -0
  15. package/dist/components/NotificationItem.module.css +136 -0
  16. package/dist/components/notification-reducer.d.ts +36 -0
  17. package/dist/components/notification-reducer.d.ts.map +1 -0
  18. package/dist/components/notification-reducer.js +74 -0
  19. package/dist/components/notification-reducer.js.map +1 -0
  20. package/dist/const.d.ts +2 -0
  21. package/dist/const.d.ts.map +1 -0
  22. package/dist/const.js +3 -0
  23. package/dist/const.js.map +1 -0
  24. package/dist/context.d.ts +19 -0
  25. package/dist/context.d.ts.map +1 -0
  26. package/dist/context.js +20 -0
  27. package/dist/context.js.map +1 -0
  28. package/dist/email/default-email.d.ts +10 -0
  29. package/dist/email/default-email.d.ts.map +1 -0
  30. package/dist/email/default-email.js +34 -0
  31. package/dist/email/default-email.js.map +1 -0
  32. package/dist/email/email-token.d.ts +17 -0
  33. package/dist/email/email-token.d.ts.map +1 -0
  34. package/dist/email/email-token.js +26 -0
  35. package/dist/email/email-token.js.map +1 -0
  36. package/dist/email/index.d.ts +2 -0
  37. package/dist/email/index.d.ts.map +1 -0
  38. package/dist/email/index.js +3 -0
  39. package/dist/email/index.js.map +1 -0
  40. package/dist/email/send-email.d.ts +11 -0
  41. package/dist/email/send-email.d.ts.map +1 -0
  42. package/dist/email/send-email.js +48 -0
  43. package/dist/email/send-email.js.map +1 -0
  44. package/dist/endpoints/delete-notification.d.ts +3 -0
  45. package/dist/endpoints/delete-notification.d.ts.map +1 -0
  46. package/dist/endpoints/delete-notification.js +20 -0
  47. package/dist/endpoints/delete-notification.js.map +1 -0
  48. package/dist/endpoints/email-unsubscribe.d.ts +2 -0
  49. package/dist/endpoints/email-unsubscribe.d.ts.map +1 -0
  50. package/dist/endpoints/email-unsubscribe.js +39 -0
  51. package/dist/endpoints/email-unsubscribe.js.map +1 -0
  52. package/dist/endpoints/map-notification.d.ts +5 -0
  53. package/dist/endpoints/map-notification.d.ts.map +1 -0
  54. package/dist/endpoints/map-notification.js +19 -0
  55. package/dist/endpoints/map-notification.js.map +1 -0
  56. package/dist/endpoints/mark-all-read.d.ts +3 -0
  57. package/dist/endpoints/mark-all-read.d.ts.map +1 -0
  58. package/dist/endpoints/mark-all-read.js +36 -0
  59. package/dist/endpoints/mark-all-read.js.map +1 -0
  60. package/dist/endpoints/mark-read.d.ts +3 -0
  61. package/dist/endpoints/mark-read.d.ts.map +1 -0
  62. package/dist/endpoints/mark-read.js +23 -0
  63. package/dist/endpoints/mark-read.js.map +1 -0
  64. package/dist/endpoints/open-notification.d.ts +7 -0
  65. package/dist/endpoints/open-notification.d.ts.map +1 -0
  66. package/dist/endpoints/open-notification.js +65 -0
  67. package/dist/endpoints/open-notification.js.map +1 -0
  68. package/dist/endpoints/read-notifications.d.ts +3 -0
  69. package/dist/endpoints/read-notifications.d.ts.map +1 -0
  70. package/dist/endpoints/read-notifications.js +39 -0
  71. package/dist/endpoints/read-notifications.js.map +1 -0
  72. package/dist/endpoints/unread-notifications.d.ts +3 -0
  73. package/dist/endpoints/unread-notifications.d.ts.map +1 -0
  74. package/dist/endpoints/unread-notifications.js +69 -0
  75. package/dist/endpoints/unread-notifications.js.map +1 -0
  76. package/dist/endpoints/unsubscribe.d.ts +2 -0
  77. package/dist/endpoints/unsubscribe.d.ts.map +1 -0
  78. package/dist/endpoints/unsubscribe.js +17 -0
  79. package/dist/endpoints/unsubscribe.js.map +1 -0
  80. package/dist/endpoints/update-preferences.d.ts +2 -0
  81. package/dist/endpoints/update-preferences.d.ts.map +1 -0
  82. package/dist/endpoints/update-preferences.js +33 -0
  83. package/dist/endpoints/update-preferences.js.map +1 -0
  84. package/dist/entities.d.ts +7 -0
  85. package/dist/entities.d.ts.map +1 -0
  86. package/dist/entities.js +161 -0
  87. package/dist/entities.js.map +1 -0
  88. package/dist/exports/client.d.ts +2 -0
  89. package/dist/exports/client.d.ts.map +1 -0
  90. package/dist/exports/client.js +4 -0
  91. package/dist/exports/client.js.map +1 -0
  92. package/dist/exports/rsc.d.ts +2 -0
  93. package/dist/exports/rsc.d.ts.map +1 -0
  94. package/dist/exports/rsc.js +3 -0
  95. package/dist/exports/rsc.js.map +1 -0
  96. package/dist/helpers.d.ts +14 -0
  97. package/dist/helpers.d.ts.map +1 -0
  98. package/dist/helpers.js +50 -0
  99. package/dist/helpers.js.map +1 -0
  100. package/dist/index.d.ts +19 -0
  101. package/dist/index.d.ts.map +1 -0
  102. package/dist/index.js +75 -0
  103. package/dist/index.js.map +1 -0
  104. package/dist/internals/index.d.ts +30 -0
  105. package/dist/internals/index.d.ts.map +1 -0
  106. package/dist/internals/index.js +81 -0
  107. package/dist/internals/index.js.map +1 -0
  108. package/dist/internals/procedure.d.ts +34 -0
  109. package/dist/internals/procedure.d.ts.map +1 -0
  110. package/dist/internals/procedure.js +111 -0
  111. package/dist/internals/procedure.js.map +1 -0
  112. package/dist/internals/urls.d.ts +11 -0
  113. package/dist/internals/urls.d.ts.map +1 -0
  114. package/dist/internals/urls.js +23 -0
  115. package/dist/internals/urls.js.map +1 -0
  116. package/dist/internals/utils.d.ts +8 -0
  117. package/dist/internals/utils.d.ts.map +1 -0
  118. package/dist/internals/utils.js +57 -0
  119. package/dist/internals/utils.js.map +1 -0
  120. package/dist/message/builder.d.ts +23 -0
  121. package/dist/message/builder.d.ts.map +1 -0
  122. package/dist/message/builder.js +43 -0
  123. package/dist/message/builder.js.map +1 -0
  124. package/dist/message/index.d.ts +3 -0
  125. package/dist/message/index.d.ts.map +1 -0
  126. package/dist/message/index.js +4 -0
  127. package/dist/message/index.js.map +1 -0
  128. package/dist/message/resolve-message.d.ts +23 -0
  129. package/dist/message/resolve-message.d.ts.map +1 -0
  130. package/dist/message/resolve-message.js +72 -0
  131. package/dist/message/resolve-message.js.map +1 -0
  132. package/dist/payload-types.d.ts +317 -0
  133. package/dist/payload-types.d.ts.map +1 -0
  134. package/dist/payload-types.js +15 -0
  135. package/dist/payload-types.js.map +1 -0
  136. package/dist/procedures.d.ts +58 -0
  137. package/dist/procedures.d.ts.map +1 -0
  138. package/dist/procedures.js +62 -0
  139. package/dist/procedures.js.map +1 -0
  140. package/dist/types.d.ts +123 -0
  141. package/dist/types.d.ts.map +1 -0
  142. package/dist/types.js +35 -0
  143. package/dist/types.js.map +1 -0
  144. package/package.json +72 -0
package/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # payload-notifications
2
+
3
+ Multi-channel notification infrastructure for Payload CMS with document subscriptions and live messages.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/payload-notifications)](https://www.npmjs.com/package/payload-notifications)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
+
8
+ ## Overview
9
+
10
+ Payload has no built-in way to notify users about events. This plugin provides a server-side `notify()` / `subscribe()` / `getSubscribers()` API that you wire into your own hooks, endpoints, and workflows. A single `notify()` call can store an in-app notification, send an email, and fire an external callback -- each channel independent and optional. Everything is user-scoped with strict access control.
11
+
12
+ **Features**
13
+
14
+ - **Multi-channel delivery** -- in-app, email, and external callbacks from a single `notify()` call. Per-user preferences control which channels are active.
15
+ - **Document subscriptions** -- users follow collections or globals. `getSubscribers()` returns follower IDs for fan-out. Supports auto-subscribe (e.g. on first comment) and manual follows.
16
+ - **Live messages** -- template tokens (`t.actor`, `t.document('title')`, `t.meta('key')`) that resolve against fresh data at read time. Renamed users and updated titles are reflected in existing notifications.
17
+ - **External callback** -- `onNotify` hook for pushing to Slack, webhooks, queues, or anything else.
18
+ - **Admin bell component** -- optional popover UI with unread badge, mark-read, unsubscribe, and delete actions.
19
+
20
+ ## Installation
21
+
22
+ ```sh
23
+ pnpm add payload-notifications
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ```ts
29
+ // payload.config.ts
30
+ import { buildConfig } from "payload";
31
+ import { notificationsPlugin } from "payload-notifications";
32
+
33
+ export default buildConfig({
34
+ // ...
35
+ plugins: [
36
+ notificationsPlugin({
37
+ email: {
38
+ generateSubject: ({ notification }) =>
39
+ `Notification: ${notification.event}`,
40
+ generateHTML: ({ notification, recipient }) =>
41
+ `<p>Hi ${recipient.displayName}, ${notification.message}</p>`,
42
+ },
43
+ }),
44
+ ],
45
+ });
46
+ ```
47
+
48
+ ### Sending notifications
49
+
50
+ ```ts
51
+ import { notify } from "payload-notifications";
52
+
53
+ await notify(req, {
54
+ recipient: user.id,
55
+ event: "comment.created",
56
+ actor: commenter.id,
57
+ message: "Someone replied to your post",
58
+ documentReference: { entity: "collection", slug: "posts", id: post.id },
59
+ });
60
+ ```
61
+
62
+ ### Subscriptions and fan-out
63
+
64
+ ```ts
65
+ import {
66
+ subscribe,
67
+ getSubscribers,
68
+ notify,
69
+ createLiveMessage,
70
+ } from "payload-notifications";
71
+
72
+ const docRef = { entity: "collection", slug: "posts", id: post.id } as const;
73
+
74
+ await subscribe(req, { userId: commenter.id, documentReference: docRef });
75
+
76
+ const subscribers = await getSubscribers(req, docRef);
77
+ for (const recipientId of subscribers) {
78
+ await notify(req, {
79
+ recipient: recipientId,
80
+ event: "comment.created",
81
+ actor: commenter.id,
82
+ message: createLiveMessage(
83
+ (t) => t`${t.actor} commented on "${t.document("title")}"`,
84
+ ),
85
+ documentReference: docRef,
86
+ });
87
+ }
88
+ ```
89
+
90
+ ### Live messages
91
+
92
+ Static strings go stale when users rename themselves or documents get updated. Live messages store tokens that resolve against fresh data at read time:
93
+
94
+ ```ts
95
+ import { notify, createLiveMessage } from "payload-notifications";
96
+
97
+ await notify(req, {
98
+ recipient: user.id,
99
+ event: "post.updated",
100
+ actor: editor.id,
101
+ message: createLiveMessage(
102
+ (t) => t`${t.actor} edited "${t.document("title")}"`,
103
+ ),
104
+ documentReference: { entity: "collection", slug: "posts", id: post.id },
105
+ });
106
+ ```
107
+
108
+ - `t.actor` -- actor's display name (from `admin.useAsTitle` on the user collection)
109
+ - `t.document(field)` -- a field from the referenced document
110
+ - `t.meta(key)` -- a key from the notification's `meta` object
111
+
112
+ ### Options
113
+
114
+ | Option | Type | Default | Description |
115
+ | ------------------- | ------------------------- | ----------------- | ------------------------------------------------------------------- |
116
+ | `email` | `NotificationEmailConfig` | -- | `generateSubject` and `generateHTML` functions. Omit to skip email. |
117
+ | `onNotify` | `NotifactionCallback` | -- | Callback for every notification (Slack, webhooks, queues, etc). |
118
+ | `notificationsSlug` | `string` | `"notifications"` | Slug for the notifications collection. |
119
+ | `subscriptionsSlug` | `string` | `"subscriptions"` | Slug for the subscriptions collection. |
120
+ | `pollInterval` | `number` | `30` | Poll interval in seconds for the admin bell component. |
121
+
122
+ ### API
123
+
124
+ All functions are server-side and require a `PayloadRequest`:
125
+
126
+ | Export | Description |
127
+ | ------------------------------- | -------------------------------------------------------------- |
128
+ | `notify(req, input)` | Deliver a notification. Respects per-user channel preferences. |
129
+ | `subscribe(req, opts)` | Subscribe a user to a document. Deduplicates automatically. |
130
+ | `unsubscribe(req, userId, ref)` | Remove a subscription. |
131
+ | `getSubscribers(req, ref)` | Return all user IDs subscribed to a document. |
132
+ | `createLiveMessage(fn)` | Build a serializable message template with dynamic tokens. |
133
+
134
+ ## Contributing
135
+
136
+ This plugin lives in the [payload-plugins](https://github.com/davincicoding-org/payload-plugins) monorepo.
137
+
138
+ ### Development
139
+
140
+ ```sh
141
+ pnpm install
142
+
143
+ # watch this plugin for changes
144
+ pnpm --filter payload-notifications dev
145
+
146
+ # run the Payload dev app (in a second terminal)
147
+ pnpm --filter sandbox dev
148
+ ```
149
+
150
+ The `sandbox/` directory is a Next.js + Payload app that imports plugins via `workspace:*` -- use it to test changes locally.
151
+
152
+ ### Code quality
153
+
154
+ - **Formatting & linting** -- handled by [Biome](https://biomejs.dev/), enforced on commit via husky + lint-staged.
155
+ - **Commits** -- must follow [Conventional Commits](https://www.conventionalcommits.org/) with a valid scope (e.g. `fix(payload-notifications): ...`).
156
+ - **Changesets** -- please include a [changeset](https://github.com/changesets/changesets) in your PR by running `pnpm release`.
157
+
158
+ ### Issues & PRs
159
+
160
+ Bug reports and feature requests are welcome -- [open an issue](https://github.com/davincicoding-org/payload-plugins/issues).
161
+
162
+ ## License
163
+
164
+ MIT
package/dist/api.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { type DocumentID, type DocumentReference } from './internals/index.js';
2
+ import type { PayloadRequest, TypeWithID } from 'payload';
3
+ import type { NotifyInput } from './types';
4
+ export declare function notify<Actor extends DocumentID | null>(req: PayloadRequest, input: NotifyInput<Actor>): Promise<void>;
5
+ export declare function subscribe(req: PayloadRequest, { userId, documentReference, }: {
6
+ userId: string | number;
7
+ documentReference: DocumentReference;
8
+ }): Promise<void>;
9
+ export declare function unsubscribe(req: PayloadRequest, userId: string | number, documentReference: DocumentReference): Promise<void>;
10
+ export declare function getSubscribers(req: PayloadRequest, documentReference: DocumentReference): Promise<TypeWithID['id'][]>;
11
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EACf,KAAK,iBAAiB,EAEvB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAS,MAAM,SAAS,CAAC;AAKjE,OAAO,KAAK,EAEV,WAAW,EAGZ,MAAM,SAAS,CAAC;AAEjB,wBAAsB,MAAM,CAAC,KAAK,SAAS,UAAU,GAAG,IAAI,EAC1D,GAAG,EAAE,cAAc,EACnB,KAAK,EAAE,WAAW,CAAC,KAAK,CAAC,GACxB,OAAO,CAAC,IAAI,CAAC,CAyEf;AAED,wBAAsB,SAAS,CAC7B,GAAG,EAAE,cAAc,EACnB,EACE,MAAM,EACN,iBAAiB,GAClB,EAAE;IACD,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,iBAAiB,EAAE,iBAAiB,CAAC;CACtC,GACA,OAAO,CAAC,IAAI,CAAC,CAsBf;AAED,wBAAsB,WAAW,CAC/B,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,MAAM,GAAG,MAAM,EACvB,iBAAiB,EAAE,iBAAiB,GACnC,OAAO,CAAC,IAAI,CAAC,CAWf;AAED,wBAAsB,cAAc,CAClC,GAAG,EAAE,cAAc,EACnB,iBAAiB,EAAE,iBAAiB,GACnC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,CAgB7B"}
package/dist/api.js ADDED
@@ -0,0 +1,144 @@
1
+ import { fetchDocumentByReference } from './internals/index.js';
2
+ import { getPluginContext } from './context';
3
+ import { sendNotificationEmail } from './email';
4
+ import { resolveUser, toStoredReference } from './helpers';
5
+ import { resolveMessageAtReadTime, toMessage } from './message';
6
+ export async function notify(req, input) {
7
+ const ctx = getPluginContext(req.payload.config);
8
+ if (!req.payload.config.admin?.user) return;
9
+ // Resolve actor display name if provided
10
+ const actor = input.actor ? await resolveUser(req.payload, input.actor) : null;
11
+ // Fetch the referenced document if provided
12
+ const document = input.documentReference ? await fetchDocumentByReference(req.payload, input.documentReference) : undefined;
13
+ // Build message context from resolved data
14
+ const messageContext = {
15
+ actor: actor,
16
+ document: document,
17
+ meta: input.meta
18
+ };
19
+ // Convert message to stored format and resolve a plain string for email/callback
20
+ const serializedMessage = toMessage(input.message, messageContext);
21
+ const resolvedMessageString = resolveMessageAtReadTime(serializedMessage, messageContext);
22
+ const recipient = await resolveUser(req.payload, input.recipient);
23
+ const notificationDoc = await req.payload.create({
24
+ collection: ctx.collectionSlugs.notifications,
25
+ data: {
26
+ recipient: input.recipient,
27
+ event: input.event,
28
+ actor: input.actor,
29
+ message: serializedMessage,
30
+ url: input.url,
31
+ meta: input.meta,
32
+ documentReference: input.documentReference ? toStoredReference(input.documentReference) : undefined
33
+ },
34
+ req
35
+ });
36
+ if (ctx.email && recipient.notificationPreferences?.emailEnabled) {
37
+ await sendNotificationEmail(req, {
38
+ emailConfig: ctx.email,
39
+ notification: {
40
+ message: resolvedMessageString,
41
+ event: input.event
42
+ },
43
+ recipient,
44
+ notificationId: notificationDoc.id,
45
+ documentReference: input.documentReference
46
+ });
47
+ }
48
+ if (ctx.onNotify) {
49
+ try {
50
+ await ctx.onNotify({
51
+ req,
52
+ notification: {
53
+ message: resolvedMessageString,
54
+ event: input.event
55
+ },
56
+ recipient
57
+ });
58
+ } catch (err) {
59
+ console.error('[payload-notifications] onNotify callback failed:', err);
60
+ }
61
+ }
62
+ }
63
+ export async function subscribe(req, { userId, documentReference }) {
64
+ const ctx = getPluginContext(req.payload.config);
65
+ const ref = toStoredReference(documentReference);
66
+ const existing = await req.payload.find({
67
+ collection: ctx.collectionSlugs.subscriptions,
68
+ where: {
69
+ and: [
70
+ {
71
+ user: {
72
+ equals: userId
73
+ }
74
+ },
75
+ ...documentReferenceWhere(ref)
76
+ ]
77
+ },
78
+ limit: 1
79
+ });
80
+ if (existing.totalDocs > 0) return;
81
+ await req.payload.create({
82
+ collection: ctx.collectionSlugs.subscriptions,
83
+ data: {
84
+ user: userId,
85
+ documentReference: ref
86
+ },
87
+ req
88
+ });
89
+ }
90
+ export async function unsubscribe(req, userId, documentReference) {
91
+ const ctx = getPluginContext(req.payload.config);
92
+ const ref = toStoredReference(documentReference);
93
+ await req.payload.delete({
94
+ collection: ctx.collectionSlugs.subscriptions,
95
+ where: {
96
+ and: [
97
+ {
98
+ user: {
99
+ equals: userId
100
+ }
101
+ },
102
+ ...documentReferenceWhere(ref)
103
+ ]
104
+ },
105
+ req
106
+ });
107
+ }
108
+ export async function getSubscribers(req, documentReference) {
109
+ const ctx = getPluginContext(req.payload.config);
110
+ const ref = toStoredReference(documentReference);
111
+ const results = await req.payload.find({
112
+ collection: ctx.collectionSlugs.subscriptions,
113
+ where: {
114
+ and: documentReferenceWhere(ref)
115
+ },
116
+ limit: 0,
117
+ depth: 0
118
+ });
119
+ return results.docs.map(({ user })=>typeof user === 'object' ? user.id : user);
120
+ }
121
+ /** Build the where clause to match a stored document reference. */ function documentReferenceWhere(ref) {
122
+ const conditions = [
123
+ {
124
+ 'documentReference.entity': {
125
+ equals: ref.entity
126
+ }
127
+ },
128
+ {
129
+ 'documentReference.slug': {
130
+ equals: ref.slug
131
+ }
132
+ }
133
+ ];
134
+ if (ref.documentId) {
135
+ conditions.push({
136
+ 'documentReference.documentId': {
137
+ equals: ref.documentId
138
+ }
139
+ });
140
+ }
141
+ return conditions;
142
+ }
143
+
144
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/api.ts"],"sourcesContent":["import {\n type DocumentID,\n type DocumentReference,\n fetchDocumentByReference,\n} from '@repo/common';\nimport type { PayloadRequest, TypeWithID, Where } from 'payload';\nimport { getPluginContext } from './context';\nimport { sendNotificationEmail } from './email';\nimport { resolveUser, toStoredReference } from './helpers';\nimport { resolveMessageAtReadTime, toMessage } from './message';\nimport type {\n MessageContext,\n NotifyInput,\n ResolvedActor,\n StoredDocumentReference,\n} from './types';\n\nexport async function notify<Actor extends DocumentID | null>(\n req: PayloadRequest,\n input: NotifyInput<Actor>,\n): Promise<void> {\n const ctx = getPluginContext(req.payload.config);\n if (!req.payload.config.admin?.user) return;\n\n // Resolve actor display name if provided\n const actor = input.actor\n ? await resolveUser(req.payload, input.actor)\n : null;\n\n // Fetch the referenced document if provided\n const document = input.documentReference\n ? await fetchDocumentByReference(req.payload, input.documentReference)\n : undefined;\n\n // Build message context from resolved data\n const messageContext: MessageContext<Actor> = {\n actor: actor as Actor extends DocumentID ? ResolvedActor : null,\n document: document as Record<string, unknown> | undefined,\n meta: input.meta,\n };\n\n // Convert message to stored format and resolve a plain string for email/callback\n const serializedMessage = toMessage(input.message, messageContext);\n const resolvedMessageString = resolveMessageAtReadTime(\n serializedMessage,\n messageContext,\n );\n\n const recipient = await resolveUser(req.payload, input.recipient);\n\n const notificationDoc = await req.payload.create({\n collection: ctx.collectionSlugs.notifications as 'notifications',\n data: {\n recipient: input.recipient as string,\n event: input.event,\n actor: input.actor as string,\n message: serializedMessage,\n url: input.url,\n meta: input.meta,\n documentReference: input.documentReference\n ? toStoredReference(input.documentReference)\n : undefined,\n },\n req,\n });\n\n if (ctx.email && recipient.notificationPreferences?.emailEnabled) {\n await sendNotificationEmail(req, {\n emailConfig: ctx.email,\n notification: {\n message: resolvedMessageString,\n event: input.event,\n },\n recipient,\n notificationId: notificationDoc.id,\n documentReference: input.documentReference,\n });\n }\n\n if (ctx.onNotify) {\n try {\n await ctx.onNotify({\n req,\n notification: {\n message: resolvedMessageString,\n event: input.event,\n },\n recipient,\n });\n } catch (err) {\n console.error('[payload-notifications] onNotify callback failed:', err);\n }\n }\n}\n\nexport async function subscribe(\n req: PayloadRequest,\n {\n userId,\n documentReference,\n }: {\n userId: string | number;\n documentReference: DocumentReference;\n },\n): Promise<void> {\n const ctx = getPluginContext(req.payload.config);\n const ref = toStoredReference(documentReference);\n\n const existing = await req.payload.find({\n collection: ctx.collectionSlugs.subscriptions as 'subscriptions',\n where: {\n and: [{ user: { equals: userId } }, ...documentReferenceWhere(ref)],\n },\n limit: 1,\n });\n\n if (existing.totalDocs > 0) return;\n\n await req.payload.create({\n collection: ctx.collectionSlugs.subscriptions as 'subscriptions',\n data: {\n user: userId as string,\n documentReference: ref,\n },\n req,\n });\n}\n\nexport async function unsubscribe(\n req: PayloadRequest,\n userId: string | number,\n documentReference: DocumentReference,\n): Promise<void> {\n const ctx = getPluginContext(req.payload.config);\n const ref = toStoredReference(documentReference);\n\n await req.payload.delete({\n collection: ctx.collectionSlugs.subscriptions as 'subscriptions',\n where: {\n and: [{ user: { equals: userId } }, ...documentReferenceWhere(ref)],\n },\n req,\n });\n}\n\nexport async function getSubscribers(\n req: PayloadRequest,\n documentReference: DocumentReference,\n): Promise<TypeWithID['id'][]> {\n const ctx = getPluginContext(req.payload.config);\n const ref = toStoredReference(documentReference);\n\n const results = await req.payload.find({\n collection: ctx.collectionSlugs.subscriptions as 'subscriptions',\n where: {\n and: documentReferenceWhere(ref),\n },\n limit: 0,\n depth: 0,\n });\n\n return results.docs.map(({ user }) =>\n typeof user === 'object' ? user.id : user,\n );\n}\n\n/** Build the where clause to match a stored document reference. */\nfunction documentReferenceWhere(ref: StoredDocumentReference): Where[] {\n const conditions: Where[] = [\n { 'documentReference.entity': { equals: ref.entity } },\n { 'documentReference.slug': { equals: ref.slug } },\n ];\n if (ref.documentId) {\n conditions.push({\n 'documentReference.documentId': { equals: ref.documentId },\n });\n }\n return conditions;\n}\n"],"names":["fetchDocumentByReference","getPluginContext","sendNotificationEmail","resolveUser","toStoredReference","resolveMessageAtReadTime","toMessage","notify","req","input","ctx","payload","config","admin","user","actor","document","documentReference","undefined","messageContext","meta","serializedMessage","message","resolvedMessageString","recipient","notificationDoc","create","collection","collectionSlugs","notifications","data","event","url","email","notificationPreferences","emailEnabled","emailConfig","notification","notificationId","id","onNotify","err","console","error","subscribe","userId","ref","existing","find","subscriptions","where","and","equals","documentReferenceWhere","limit","totalDocs","unsubscribe","delete","getSubscribers","results","depth","docs","map","conditions","entity","slug","documentId","push"],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { ResolvedPluginOptions } from '../types';
2
+ export type NotificationBellProps = ResolvedPluginOptions<'pollInterval'>;
3
+ export declare function NotificationBell({ pollInterval }: NotificationBellProps): import("react/jsx-runtime").JSX.Element;
4
+ //# sourceMappingURL=NotificationBell.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NotificationBell.d.ts","sourceRoot":"","sources":["../../src/components/NotificationBell.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,qBAAqB,EAA2B,MAAM,SAAS,CAAC;AAU9E,MAAM,MAAM,qBAAqB,GAAG,qBAAqB,CAAC,cAAc,CAAC,CAAC;AAE1E,wBAAgB,gBAAgB,CAAC,EAAE,YAAY,EAAE,EAAE,qBAAqB,2CA+IvE"}
@@ -0,0 +1,318 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Collapsible } from '@base-ui/react/collapsible';
4
+ import { Popover } from '@base-ui/react/popover';
5
+ import { Button, useAuth, useConfig } from '@payloadcms/ui';
6
+ import { IconAdjustments, IconBell } from '@tabler/icons-react';
7
+ import { useCallback, useEffect, useReducer, useRef, useState } from 'react';
8
+ import { toDocumentReference } from '../helpers';
9
+ import { ENDPOINTS } from '../procedures';
10
+ import styles from './NotificationBell.module.css';
11
+ import { NotificationItem } from './NotificationItem';
12
+ import { INITIAL_STATE, notificationReducer } from './notification-reducer';
13
+ export function NotificationBell({ pollInterval }) {
14
+ const { config: { routes: { api: apiRoute } } } = useConfig();
15
+ const { user } = useAuth();
16
+ const [mounted, setMounted] = useState(false);
17
+ useEffect(()=>setMounted(true), []);
18
+ const [state, dispatch] = useReducer(notificationReducer, INITIAL_STATE);
19
+ useUnreadPolling(apiRoute, pollInterval, state, dispatch, mounted);
20
+ const { loadMore, isLoadingRead } = useReadNotifications(apiRoute, state, dispatch);
21
+ const { markRead, markAllRead, deleteNotification } = useNotificationActions(apiRoute, dispatch);
22
+ const { prefs, togglePref, unsubscribe } = useNotificationPreferences(apiRoute, user);
23
+ const bellIcon = /*#__PURE__*/ _jsxs("div", {
24
+ className: styles.bellIcon,
25
+ children: [
26
+ /*#__PURE__*/ _jsx(IconBell, {
27
+ size: 20,
28
+ strokeWidth: 1.5
29
+ }),
30
+ state.unread.length > 0 && /*#__PURE__*/ _jsx("span", {
31
+ className: styles.indicator,
32
+ children: state.unread.length
33
+ })
34
+ ]
35
+ });
36
+ // Render a static bell during SSR to avoid hydration mismatch from
37
+ // Base UI's Popover portal and floating-ui context.
38
+ if (!mounted) {
39
+ return /*#__PURE__*/ _jsx(Button, {
40
+ buttonStyle: "tab",
41
+ children: bellIcon
42
+ });
43
+ }
44
+ return /*#__PURE__*/ _jsxs(Popover.Root, {
45
+ children: [
46
+ /*#__PURE__*/ _jsx(Popover.Trigger, {
47
+ render: /*#__PURE__*/ _jsx(Button, {
48
+ buttonStyle: "tab"
49
+ }),
50
+ children: bellIcon
51
+ }),
52
+ /*#__PURE__*/ _jsx(Popover.Portal, {
53
+ children: /*#__PURE__*/ _jsx(Popover.Positioner, {
54
+ align: "start",
55
+ sideOffset: 8,
56
+ children: /*#__PURE__*/ _jsxs(Popover.Popup, {
57
+ className: styles.popoverPopup,
58
+ children: [
59
+ /*#__PURE__*/ _jsx(Popover.Arrow, {
60
+ className: styles.popoverArrow,
61
+ children: /*#__PURE__*/ _jsx("svg", {
62
+ fill: "none",
63
+ height: "10",
64
+ viewBox: "0 0 20 10",
65
+ width: "20",
66
+ children: /*#__PURE__*/ _jsx("path", {
67
+ d: "M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
68
+ })
69
+ })
70
+ }),
71
+ /*#__PURE__*/ _jsxs(Popover.Viewport, {
72
+ className: styles.popoverViewport,
73
+ children: [
74
+ /*#__PURE__*/ _jsx(Collapsible.Root, {
75
+ children: /*#__PURE__*/ _jsxs("div", {
76
+ className: styles.panelTop,
77
+ children: [
78
+ /*#__PURE__*/ _jsxs("div", {
79
+ className: styles.header,
80
+ children: [
81
+ /*#__PURE__*/ _jsx("h3", {
82
+ className: styles.title,
83
+ children: "Notifications"
84
+ }),
85
+ /*#__PURE__*/ _jsx(Collapsible.Trigger, {
86
+ "aria-label": "Notification settings",
87
+ className: styles.headerAction,
88
+ type: "button",
89
+ children: /*#__PURE__*/ _jsx(IconAdjustments, {
90
+ size: 18,
91
+ strokeWidth: 1.5
92
+ })
93
+ })
94
+ ]
95
+ }),
96
+ /*#__PURE__*/ _jsx(Collapsible.Panel, {
97
+ className: styles.prefsPanel,
98
+ children: /*#__PURE__*/ _jsxs("label", {
99
+ className: styles.prefRow,
100
+ children: [
101
+ /*#__PURE__*/ _jsx("input", {
102
+ checked: prefs?.emailEnabled ?? true,
103
+ onChange: ()=>togglePref('emailEnabled'),
104
+ type: "checkbox"
105
+ }),
106
+ "Email notifications"
107
+ ]
108
+ })
109
+ })
110
+ ]
111
+ })
112
+ }),
113
+ /*#__PURE__*/ _jsxs("div", {
114
+ className: styles.sections,
115
+ children: [
116
+ state.unread.length === 0 && !state.isReadLoaded && /*#__PURE__*/ _jsx("p", {
117
+ className: styles.empty,
118
+ children: "No notifications"
119
+ }),
120
+ state.unread.map((n)=>/*#__PURE__*/ _jsx(NotificationItem, {
121
+ apiRoute: apiRoute,
122
+ notification: n,
123
+ onDelete: deleteNotification,
124
+ onMarkRead: markRead,
125
+ onUnsubscribe: unsubscribe
126
+ }, n.id)),
127
+ !state.isReadLoaded && state.hasMore && /*#__PURE__*/ _jsx("button", {
128
+ className: styles.showOlder,
129
+ onClick: loadMore,
130
+ type: "button",
131
+ children: "Show older"
132
+ }),
133
+ state.isReadLoaded && state.read.length === 0 && state.unread.length === 0 && /*#__PURE__*/ _jsx("p", {
134
+ className: styles.empty,
135
+ children: "No notifications"
136
+ }),
137
+ state.read.map((n)=>/*#__PURE__*/ _jsx(NotificationItem, {
138
+ apiRoute: apiRoute,
139
+ notification: n,
140
+ onDelete: deleteNotification,
141
+ onMarkRead: markRead,
142
+ onUnsubscribe: unsubscribe
143
+ }, n.id)),
144
+ state.isReadLoaded && state.hasMoreRead && /*#__PURE__*/ _jsx("button", {
145
+ className: styles.showOlder,
146
+ disabled: isLoadingRead,
147
+ onClick: loadMore,
148
+ type: "button",
149
+ children: isLoadingRead ? 'Loading...' : 'Show more'
150
+ })
151
+ ]
152
+ })
153
+ ]
154
+ })
155
+ ]
156
+ })
157
+ })
158
+ })
159
+ ]
160
+ });
161
+ }
162
+ // ---------------------------------------------------------------------------
163
+ // Hooks
164
+ // ---------------------------------------------------------------------------
165
+ /** Polls unread notifications with `since`-based diffing. */ function useUnreadPolling(apiRoute, pollInterval, state, dispatch, enabled) {
166
+ const timestampRef = useRef(state.pollTimestamp);
167
+ timestampRef.current = state.pollTimestamp;
168
+ const poll = useCallback(async ()=>{
169
+ try {
170
+ const since = timestampRef.current ?? undefined;
171
+ const { docs, timestamp, hasMore } = await ENDPOINTS.unread.call(apiRoute, {
172
+ since
173
+ });
174
+ if (!timestampRef.current) {
175
+ dispatch({
176
+ type: 'SET_UNREAD',
177
+ docs,
178
+ timestamp,
179
+ hasMore: hasMore ?? false
180
+ });
181
+ } else {
182
+ dispatch({
183
+ type: 'PREPEND_UNREAD',
184
+ docs,
185
+ timestamp
186
+ });
187
+ }
188
+ } catch {
189
+ // Poll will retry on next interval
190
+ }
191
+ }, [
192
+ apiRoute,
193
+ dispatch
194
+ ]);
195
+ useEffect(()=>{
196
+ if (!enabled) return;
197
+ let interval = null;
198
+ const start = ()=>{
199
+ poll();
200
+ interval = setInterval(poll, pollInterval * 1000);
201
+ };
202
+ const stop = ()=>{
203
+ if (interval) clearInterval(interval);
204
+ interval = null;
205
+ };
206
+ const onVisibilityChange = ()=>{
207
+ if (document.hidden) stop();
208
+ else start();
209
+ };
210
+ start();
211
+ document.addEventListener('visibilitychange', onVisibilityChange);
212
+ return ()=>{
213
+ stop();
214
+ document.removeEventListener('visibilitychange', onVisibilityChange);
215
+ };
216
+ }, [
217
+ poll,
218
+ pollInterval,
219
+ enabled
220
+ ]);
221
+ }
222
+ /** On-demand paginated loading of read notifications. */ function useReadNotifications(apiRoute, state, dispatch) {
223
+ const [isLoadingRead, setIsLoadingRead] = useState(false);
224
+ const nextPage = state.readPage + 1;
225
+ const loadMore = useCallback(async ()=>{
226
+ setIsLoadingRead(true);
227
+ try {
228
+ const { docs, hasNextPage } = await ENDPOINTS.read.call(apiRoute, {
229
+ page: nextPage,
230
+ limit: 10
231
+ });
232
+ dispatch({
233
+ type: 'APPEND_READ',
234
+ docs,
235
+ hasNextPage
236
+ });
237
+ } finally{
238
+ setIsLoadingRead(false);
239
+ }
240
+ }, [
241
+ apiRoute,
242
+ nextPage,
243
+ dispatch
244
+ ]);
245
+ return {
246
+ loadMore,
247
+ isLoadingRead
248
+ };
249
+ }
250
+ /** Optimistic mutations dispatched to the reducer. */ function useNotificationActions(apiRoute, dispatch) {
251
+ const markRead = useCallback(async (id)=>{
252
+ dispatch({
253
+ type: 'MARK_READ',
254
+ id,
255
+ readAt: new Date().toISOString()
256
+ });
257
+ await ENDPOINTS.markRead.call(apiRoute, {
258
+ id
259
+ });
260
+ }, [
261
+ apiRoute,
262
+ dispatch
263
+ ]);
264
+ const markAllRead = useCallback(async ()=>{
265
+ dispatch({
266
+ type: 'MARK_ALL_READ'
267
+ });
268
+ await ENDPOINTS.markAllRead.call(apiRoute);
269
+ }, [
270
+ apiRoute,
271
+ dispatch
272
+ ]);
273
+ const deleteNotification = useCallback(async (id)=>{
274
+ dispatch({
275
+ type: 'DELETE_NOTIFICATION',
276
+ id
277
+ });
278
+ await ENDPOINTS.deleteNotification.call(apiRoute, {
279
+ id
280
+ });
281
+ }, [
282
+ apiRoute,
283
+ dispatch
284
+ ]);
285
+ return {
286
+ markRead,
287
+ markAllRead,
288
+ deleteNotification
289
+ };
290
+ }
291
+ /** Reads and toggles user-level notification preferences. */ function useNotificationPreferences(apiRoute, user) {
292
+ const prefs = user?.notificationPreferences;
293
+ const togglePref = useCallback(async (field)=>{
294
+ if (!user) return;
295
+ const current = prefs?.[field] ?? true;
296
+ await ENDPOINTS.updatePreferences.call(apiRoute, {
297
+ [field]: !current
298
+ });
299
+ }, [
300
+ apiRoute,
301
+ user,
302
+ prefs
303
+ ]);
304
+ const unsubscribe = useCallback(async (ref)=>{
305
+ await ENDPOINTS.unsubscribe.call(apiRoute, {
306
+ documentReference: toDocumentReference(ref)
307
+ });
308
+ }, [
309
+ apiRoute
310
+ ]);
311
+ return {
312
+ prefs,
313
+ togglePref,
314
+ unsubscribe
315
+ };
316
+ }
317
+
318
+ //# sourceMappingURL=NotificationBell.js.map