feat: implement complete web push notification system
- Add self-hosted push notifications using VAPID (no external services) - Create PushSubscription model in database - Implement server-side push logic with web-push library - Add notification helpers for list events (items, invitations, collaborators) - Create NotificationSettings UI component for profile page - Add usePushNotifications React hook for subscription management - Create API routes for subscription management and VAPID key - Update service worker with push event handlers - Integrate notifications into item actions (add, remove, complete) - Integrate notifications into sharing actions (invite, accept, remove) - Add VAPID key generation script - Add comprehensive documentation and setup guides Notifications are sent when: - Items are added/removed/completed in shared lists - Users are invited to lists - Invitations are accepted - Collaborators are removed from lists Works on iOS 16.4+ (when installed to home screen), Android, and desktop browsers.
This commit is contained in:
201
lib/notification-helpers.ts
Normal file
201
lib/notification-helpers.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
'use server';
|
||||
|
||||
import { sendPushNotification, notifyListCollaborators } from '@/lib/push-notifications';
|
||||
import { prisma } from '@/lib/db';
|
||||
|
||||
/**
|
||||
* Notify when an item is added to a shared list
|
||||
*/
|
||||
export async function notifyItemAdded(
|
||||
listId: string,
|
||||
itemTitle: string,
|
||||
addedByUserId: string
|
||||
) {
|
||||
const list = await prisma.list.findUnique({
|
||||
where: { id: listId },
|
||||
include: { owner: true },
|
||||
});
|
||||
|
||||
if (!list) return;
|
||||
|
||||
const addedByUser = await prisma.user.findUnique({
|
||||
where: { id: addedByUserId },
|
||||
});
|
||||
|
||||
await notifyListCollaborators(
|
||||
listId,
|
||||
{
|
||||
title: `New item in "${list.title}"`,
|
||||
body: `${addedByUser?.name || 'Someone'} added "${itemTitle}"`,
|
||||
icon: '/icon-192.svg',
|
||||
tag: `item-added-${listId}`,
|
||||
data: {
|
||||
url: `/list/${listId}`,
|
||||
listId,
|
||||
action: 'item-added',
|
||||
},
|
||||
},
|
||||
addedByUserId // Don't notify the person who added it
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify when an item is removed from a shared list
|
||||
*/
|
||||
export async function notifyItemRemoved(
|
||||
listId: string,
|
||||
itemTitle: string,
|
||||
removedByUserId: string
|
||||
) {
|
||||
const list = await prisma.list.findUnique({
|
||||
where: { id: listId },
|
||||
include: { owner: true },
|
||||
});
|
||||
|
||||
if (!list) return;
|
||||
|
||||
const removedByUser = await prisma.user.findUnique({
|
||||
where: { id: removedByUserId },
|
||||
});
|
||||
|
||||
await notifyListCollaborators(
|
||||
listId,
|
||||
{
|
||||
title: `Item removed from "${list.title}"`,
|
||||
body: `${removedByUser?.name || 'Someone'} removed "${itemTitle}"`,
|
||||
icon: '/icon-192.svg',
|
||||
tag: `item-removed-${listId}`,
|
||||
data: {
|
||||
url: `/list/${listId}`,
|
||||
listId,
|
||||
action: 'item-removed',
|
||||
},
|
||||
},
|
||||
removedByUserId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify when an item is completed
|
||||
*/
|
||||
export async function notifyItemCompleted(
|
||||
listId: string,
|
||||
itemTitle: string,
|
||||
completedByUserId: string
|
||||
) {
|
||||
const list = await prisma.list.findUnique({
|
||||
where: { id: listId },
|
||||
include: { owner: true },
|
||||
});
|
||||
|
||||
if (!list) return;
|
||||
|
||||
const completedByUser = await prisma.user.findUnique({
|
||||
where: { id: completedByUserId },
|
||||
});
|
||||
|
||||
await notifyListCollaborators(
|
||||
listId,
|
||||
{
|
||||
title: `Item completed in "${list.title}"`,
|
||||
body: `${completedByUser?.name || 'Someone'} completed "${itemTitle}"`,
|
||||
icon: '/icon-192.svg',
|
||||
tag: `item-completed-${listId}`,
|
||||
data: {
|
||||
url: `/list/${listId}`,
|
||||
listId,
|
||||
action: 'item-completed',
|
||||
},
|
||||
},
|
||||
completedByUserId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify when invited to a list
|
||||
*/
|
||||
export async function notifyListInvitation(
|
||||
invitationId: string,
|
||||
inviteeEmail: string
|
||||
) {
|
||||
const invitation = await prisma.invitation.findUnique({
|
||||
where: { id: invitationId },
|
||||
include: {
|
||||
list: true,
|
||||
inviter: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!invitation) return;
|
||||
|
||||
// Find the invitee user
|
||||
const invitee = await prisma.user.findUnique({
|
||||
where: { email: inviteeEmail },
|
||||
});
|
||||
|
||||
if (!invitee) return;
|
||||
|
||||
await sendPushNotification(invitee.id, {
|
||||
title: 'New list invitation',
|
||||
body: `${invitation.inviter.name || 'Someone'} invited you to "${invitation.list.title}"`,
|
||||
icon: '/icon-192.svg',
|
||||
tag: `invitation-${invitationId}`,
|
||||
data: {
|
||||
url: '/invitations',
|
||||
action: 'invitation-received',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify when someone accepts your invitation
|
||||
*/
|
||||
export async function notifyInvitationAccepted(
|
||||
invitationId: string
|
||||
) {
|
||||
const invitation = await prisma.invitation.findUnique({
|
||||
where: { id: invitationId },
|
||||
include: {
|
||||
list: true,
|
||||
receiver: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!invitation || !invitation.receiver) return;
|
||||
|
||||
await sendPushNotification(invitation.inviterId, {
|
||||
title: 'Invitation accepted',
|
||||
body: `${invitation.receiver.name || invitation.email} joined "${invitation.list.title}"`,
|
||||
icon: '/icon-192.svg',
|
||||
tag: `invitation-accepted-${invitationId}`,
|
||||
data: {
|
||||
url: `/list/${invitation.listId}`,
|
||||
listId: invitation.listId,
|
||||
action: 'invitation-accepted',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify when removed from a list
|
||||
*/
|
||||
export async function notifyRemovedFromList(
|
||||
userId: string,
|
||||
listTitle: string,
|
||||
removedByUserId: string
|
||||
) {
|
||||
const removedByUser = await prisma.user.findUnique({
|
||||
where: { id: removedByUserId },
|
||||
});
|
||||
|
||||
await sendPushNotification(userId, {
|
||||
title: 'Removed from list',
|
||||
body: `${removedByUser?.name || 'The owner'} removed you from "${listTitle}"`,
|
||||
icon: '/icon-192.svg',
|
||||
tag: `removed-from-list`,
|
||||
data: {
|
||||
url: '/',
|
||||
action: 'removed-from-list',
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user