mirror of
https://github.com/xtr-dev/payload-notifications.git
synced 2025-12-10 02:43:23 +00:00
247 lines
8.6 KiB
TypeScript
247 lines
8.6 KiB
TypeScript
import type { CollectionConfig, Field } from 'payload'
|
|
import type { NotificationsPluginOptions } from '../types'
|
|
import { WebPushManager } from '../utils/webPush'
|
|
import { defaultNotificationTransformer } from '../utils/richTextExtractor'
|
|
|
|
/**
|
|
* Creates the notifications collection configuration
|
|
*/
|
|
export function createNotificationsCollection(options: NotificationsPluginOptions): CollectionConfig {
|
|
const slug = 'notifications'
|
|
const labels = {
|
|
singular: 'Notification',
|
|
plural: 'Notifications',
|
|
}
|
|
|
|
if (options.channels.length === 0) {
|
|
throw new Error('No channels defined for notifications plugin')
|
|
}
|
|
|
|
// Default access control - authenticated users can read, admins can manage
|
|
const access: CollectionConfig['access'] = {
|
|
read: ({ req }: { req: any }) => Boolean(req.user),
|
|
create: ({ req }: { req: any }) => Boolean(req.user),
|
|
update: ({ req }: { req: any }) => Boolean(req.user),
|
|
delete: ({ req }: { req: any }) => Boolean(req.user?.role === 'admin'),
|
|
}
|
|
|
|
// Build core fields
|
|
const allFields: Field[] = [
|
|
{
|
|
name: 'title',
|
|
type: 'text',
|
|
label: 'Title',
|
|
required: true,
|
|
admin: {
|
|
description: 'The notification title that will be displayed to users',
|
|
},
|
|
},
|
|
{
|
|
name: 'message',
|
|
type: 'richText',
|
|
label: 'Message',
|
|
required: true,
|
|
admin: {
|
|
description: 'The notification message content',
|
|
},
|
|
},
|
|
{
|
|
name: 'recipient',
|
|
type: 'relationship',
|
|
relationTo: 'users',
|
|
label: 'Recipient',
|
|
required: false,
|
|
admin: {
|
|
description: 'The user who should receive this notification (optional if using custom recipient fields)',
|
|
},
|
|
},
|
|
{
|
|
name: 'channel',
|
|
type: 'select',
|
|
label: 'Channel',
|
|
options: options.channels.map(channel => ({
|
|
label: channel.name,
|
|
value: channel.id,
|
|
})),
|
|
required: false,
|
|
admin: {
|
|
description: 'The notification channel - only subscribers to this channel will receive the notification',
|
|
position: 'sidebar',
|
|
},
|
|
},
|
|
{
|
|
name: 'isRead',
|
|
type: 'checkbox',
|
|
label: 'Read',
|
|
defaultValue: false,
|
|
admin: {
|
|
description: 'Whether this notification has been read by the recipient',
|
|
position: 'sidebar',
|
|
},
|
|
},
|
|
{
|
|
name: 'readAt',
|
|
type: 'date',
|
|
label: 'Read At',
|
|
admin: {
|
|
description: 'When this notification was marked as read',
|
|
position: 'sidebar',
|
|
condition: (_: any, siblingData: any) => siblingData?.isRead,
|
|
},
|
|
},
|
|
]
|
|
|
|
const config: CollectionConfig = {
|
|
slug,
|
|
labels,
|
|
admin: {
|
|
useAsTitle: 'title',
|
|
defaultColumns: ['title', 'recipient', 'isRead', 'createdAt'],
|
|
description: 'Manage user notifications and messaging',
|
|
},
|
|
fields: allFields,
|
|
access,
|
|
timestamps: true,
|
|
}
|
|
|
|
// Add hooks for automatic push notifications if web push is enabled
|
|
if (options.webPush?.enabled && options.webPush.autoPush) {
|
|
config.hooks = {
|
|
afterChange: [
|
|
async ({ doc, operation, req }) => {
|
|
// Only send push notifications for new notifications
|
|
if (operation !== 'create') return
|
|
|
|
try {
|
|
const webPushConfig = options.webPush!
|
|
const pushManager = new WebPushManager(webPushConfig, req.payload)
|
|
|
|
// Transform notification content using custom transformer or default
|
|
const transformer = webPushConfig.transformNotification || defaultNotificationTransformer
|
|
const pushContent = transformer(doc)
|
|
|
|
console.log('[Notifications Plugin] Sending push notification for notification:', doc.id)
|
|
console.log('[Notifications Plugin] Push content:', pushContent)
|
|
|
|
let results: Array<{ success: boolean; error?: any }> = []
|
|
|
|
// Check if custom findSubscriptions hook is provided
|
|
if (webPushConfig.findSubscriptions) {
|
|
// Use custom hook to find subscriptions
|
|
console.log('[Notifications Plugin] Using custom findSubscriptions hook')
|
|
const subscriptions = await webPushConfig.findSubscriptions(doc, req.payload)
|
|
|
|
if (!subscriptions || subscriptions.length === 0) {
|
|
console.log('[Notifications Plugin] No subscriptions found via custom hook')
|
|
return
|
|
}
|
|
|
|
// Send notifications directly to the found subscriptions
|
|
const notificationPayload = JSON.stringify({
|
|
title: pushContent.title,
|
|
body: pushContent.body,
|
|
icon: pushContent.icon || '/icon-192x192.png',
|
|
badge: pushContent.badge || '/badge-72x72.png',
|
|
image: pushContent.image,
|
|
data: pushContent.data,
|
|
actions: pushContent.actions,
|
|
tag: pushContent.tag,
|
|
requireInteraction: pushContent.requireInteraction || false,
|
|
timestamp: Date.now(),
|
|
})
|
|
|
|
results = await Promise.allSettled(
|
|
subscriptions.map(async (sub: any) => {
|
|
try {
|
|
const pushSub = {
|
|
endpoint: sub.endpoint,
|
|
keys: {
|
|
p256dh: sub.p256dh,
|
|
auth: sub.auth,
|
|
},
|
|
}
|
|
await pushManager.sendNotification(pushSub, notificationPayload)
|
|
return { success: true }
|
|
} catch (error: any) {
|
|
// Handle expired/invalid subscriptions
|
|
if (error.statusCode === 410 || error.statusCode === 404) {
|
|
await req.payload.update({
|
|
collection: 'push-subscriptions',
|
|
id: sub.id,
|
|
data: { isActive: false },
|
|
})
|
|
}
|
|
return { success: false, error }
|
|
}
|
|
})
|
|
).then(results =>
|
|
results.map((result) =>
|
|
result.status === 'fulfilled'
|
|
? result.value
|
|
: { success: false, error: result.reason }
|
|
)
|
|
)
|
|
} else {
|
|
// Use default behavior - send to recipient user (if recipient is provided)
|
|
if (!doc.recipient) {
|
|
console.warn('[Notifications Plugin] No recipient found and no findSubscriptions hook provided - skipping push notification')
|
|
return
|
|
}
|
|
|
|
let recipientId: string|number
|
|
|
|
if (typeof doc.recipient === 'string' || typeof doc.recipient === 'number') {
|
|
recipientId = doc.recipient
|
|
} else if (doc.recipient?.id) {
|
|
recipientId = doc.recipient.id
|
|
} else {
|
|
console.warn('[Notifications Plugin] No valid recipient found for push notification')
|
|
return
|
|
}
|
|
|
|
console.log('[Notifications Plugin] Using default user-based recipient logic for:', recipientId)
|
|
|
|
// Send push notification to the recipient user
|
|
results = await pushManager.sendToRecipient(
|
|
recipientId,
|
|
pushContent.title,
|
|
pushContent.body,
|
|
{
|
|
icon: pushContent.icon,
|
|
badge: pushContent.badge,
|
|
image: pushContent.image,
|
|
data: pushContent.data,
|
|
actions: pushContent.actions,
|
|
tag: pushContent.tag,
|
|
requireInteraction: pushContent.requireInteraction,
|
|
channel: doc.channel,
|
|
recipientType: 'user',
|
|
}
|
|
)
|
|
}
|
|
|
|
const successful = results.filter(r => r.success).length
|
|
const failed = results.filter(r => !r.success).length
|
|
|
|
console.log(`[Notifications Plugin] Push notification results: ${successful} sent, ${failed} failed`)
|
|
|
|
if (failed > 0) {
|
|
console.warn('[Notifications Plugin] Some push notifications failed:',
|
|
results.filter(r => !r.success).map(r => r.error)
|
|
)
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('[Notifications Plugin] Error sending push notification:', error)
|
|
// Don't throw error - we don't want to prevent notification creation
|
|
}
|
|
},
|
|
],
|
|
}
|
|
}
|
|
|
|
return options.collectionOverrides?.notifications ?
|
|
options.collectionOverrides.notifications(config) :
|
|
config
|
|
}
|