feat:add Web Push notification system
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { findComment, user } from '$lib/server/db/schema';
|
||||
import { findComment, user, find } from '$lib/server/db/schema';
|
||||
import { eq, desc } from 'drizzle-orm';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { notificationService } from '$lib/server/notifications';
|
||||
import { pushService } from '$lib/server/push';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, locals }) => {
|
||||
const session = locals.session;
|
||||
@@ -108,6 +110,54 @@ export const POST: RequestHandler = async ({ params, locals, request }) => {
|
||||
return json({ success: false, error: 'Failed to create comment' }, { status: 500 });
|
||||
}
|
||||
|
||||
// Send notification to find owner if not self-comment
|
||||
const findData = await db.select().from(find).where(eq(find.id, findId)).limit(1);
|
||||
|
||||
if (findData.length > 0 && findData[0].userId !== session.userId) {
|
||||
const findOwner = findData[0];
|
||||
const shouldNotify = await notificationService.shouldNotify(findOwner.userId, 'find_commented');
|
||||
|
||||
if (shouldNotify) {
|
||||
// Get commenter's username
|
||||
const commenterUser = await db
|
||||
.select({ username: user.username })
|
||||
.from(user)
|
||||
.where(eq(user.id, session.userId))
|
||||
.limit(1);
|
||||
|
||||
const commenterUsername = commenterUser[0]?.username || 'Someone';
|
||||
const findTitle = findOwner.title || 'your find';
|
||||
|
||||
await notificationService.createNotification({
|
||||
userId: findOwner.userId,
|
||||
type: 'find_commented',
|
||||
title: 'New comment on your find',
|
||||
message: `${commenterUsername} commented on: ${findTitle}`,
|
||||
data: {
|
||||
findId: findOwner.id,
|
||||
commentId,
|
||||
commenterId: session.userId,
|
||||
commenterUsername,
|
||||
findTitle,
|
||||
commentContent: content.trim().substring(0, 100)
|
||||
}
|
||||
});
|
||||
|
||||
// Send push notification
|
||||
await pushService.sendPushNotification(findOwner.userId, {
|
||||
title: 'New comment on your find',
|
||||
message: `${commenterUsername} commented on: ${findTitle}`,
|
||||
url: `/?find=${findOwner.id}`,
|
||||
tag: 'find_commented',
|
||||
data: {
|
||||
findId: findOwner.id,
|
||||
commentId,
|
||||
commenterId: session.userId
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
data: commentWithUser[0]
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { findLike, find } from '$lib/server/db/schema';
|
||||
import { findLike, find, user } from '$lib/server/db/schema';
|
||||
import { eq, and, count } from 'drizzle-orm';
|
||||
import { encodeBase64url } from '@oslojs/encoding';
|
||||
import { notificationService } from '$lib/server/notifications';
|
||||
import { pushService } from '$lib/server/push';
|
||||
|
||||
function generateLikeId(): string {
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(15));
|
||||
@@ -59,6 +61,49 @@ export async function POST({
|
||||
|
||||
const likeCount = likeCountResult[0]?.count ?? 0;
|
||||
|
||||
// Send notification to find owner if not self-like
|
||||
const findOwner = existingFind[0];
|
||||
if (findOwner.userId !== locals.user.id) {
|
||||
const shouldNotify = await notificationService.shouldNotify(findOwner.userId, 'find_liked');
|
||||
|
||||
if (shouldNotify) {
|
||||
// Get liker's username
|
||||
const likerUser = await db
|
||||
.select({ username: user.username })
|
||||
.from(user)
|
||||
.where(eq(user.id, locals.user.id))
|
||||
.limit(1);
|
||||
|
||||
const likerUsername = likerUser[0]?.username || 'Someone';
|
||||
const findTitle = findOwner.title || 'your find';
|
||||
|
||||
await notificationService.createNotification({
|
||||
userId: findOwner.userId,
|
||||
type: 'find_liked',
|
||||
title: 'Someone liked your find',
|
||||
message: `${likerUsername} liked your find: ${findTitle}`,
|
||||
data: {
|
||||
findId: findOwner.id,
|
||||
likerId: locals.user.id,
|
||||
likerUsername,
|
||||
findTitle
|
||||
}
|
||||
});
|
||||
|
||||
// Send push notification
|
||||
await pushService.sendPushNotification(findOwner.userId, {
|
||||
title: 'Someone liked your find',
|
||||
message: `${likerUsername} liked your find: ${findTitle}`,
|
||||
url: `/?find=${findOwner.id}`,
|
||||
tag: 'find_liked',
|
||||
data: {
|
||||
findId: findOwner.id,
|
||||
likerId: locals.user.id
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
likeId,
|
||||
|
||||
@@ -5,6 +5,8 @@ import { friendship, user } from '$lib/server/db/schema';
|
||||
import { eq, and, or } from 'drizzle-orm';
|
||||
import { encodeBase64url } from '@oslojs/encoding';
|
||||
import { getSignedR2Url } from '$lib/server/r2';
|
||||
import { notificationService } from '$lib/server/notifications';
|
||||
import { pushService } from '$lib/server/push';
|
||||
|
||||
function generateFriendshipId(): string {
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(15));
|
||||
@@ -180,6 +182,34 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
})
|
||||
.returning();
|
||||
|
||||
// Send notification to friend
|
||||
const shouldNotify = await notificationService.shouldNotify(friendId, 'friend_request');
|
||||
if (shouldNotify) {
|
||||
await notificationService.createNotification({
|
||||
userId: friendId,
|
||||
type: 'friend_request',
|
||||
title: 'New friend request',
|
||||
message: `${locals.user.username} sent you a friend request`,
|
||||
data: {
|
||||
friendshipId: newFriendship[0].id,
|
||||
senderId: locals.user.id,
|
||||
senderUsername: locals.user.username
|
||||
}
|
||||
});
|
||||
|
||||
// Send push notification
|
||||
await pushService.sendPushNotification(friendId, {
|
||||
title: 'New friend request',
|
||||
message: `${locals.user.username} sent you a friend request`,
|
||||
url: '/friends',
|
||||
tag: 'friend_request',
|
||||
data: {
|
||||
friendshipId: newFriendship[0].id,
|
||||
senderId: locals.user.id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return json({ success: true, friendship: newFriendship[0] });
|
||||
} catch (err) {
|
||||
console.error('Error sending friend request:', err);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { db } from '$lib/server/db';
|
||||
import { friendship } from '$lib/server/db/schema';
|
||||
import { friendship, user } from '$lib/server/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { notificationService } from '$lib/server/notifications';
|
||||
import { pushService } from '$lib/server/push';
|
||||
|
||||
export const PUT: RequestHandler = async ({ params, request, locals }) => {
|
||||
if (!locals.user) {
|
||||
@@ -72,6 +74,47 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
|
||||
.where(eq(friendship.id, friendshipId))
|
||||
.returning();
|
||||
|
||||
// Send notification if friend request was accepted
|
||||
if (action === 'accept') {
|
||||
const senderId = friendshipRecord.userId;
|
||||
const shouldNotify = await notificationService.shouldNotify(senderId, 'friend_accepted');
|
||||
|
||||
if (shouldNotify) {
|
||||
// Get accepter's username
|
||||
const accepterUser = await db
|
||||
.select({ username: user.username })
|
||||
.from(user)
|
||||
.where(eq(user.id, locals.user.id))
|
||||
.limit(1);
|
||||
|
||||
const accepterUsername = accepterUser[0]?.username || 'Someone';
|
||||
|
||||
await notificationService.createNotification({
|
||||
userId: senderId,
|
||||
type: 'friend_accepted',
|
||||
title: 'Friend request accepted',
|
||||
message: `${accepterUsername} accepted your friend request`,
|
||||
data: {
|
||||
friendshipId: friendshipRecord.id,
|
||||
accepterId: locals.user.id,
|
||||
accepterUsername
|
||||
}
|
||||
});
|
||||
|
||||
// Send push notification
|
||||
await pushService.sendPushNotification(senderId, {
|
||||
title: 'Friend request accepted',
|
||||
message: `${accepterUsername} accepted your friend request`,
|
||||
url: '/friends',
|
||||
tag: 'friend_accepted',
|
||||
data: {
|
||||
friendshipId: friendshipRecord.id,
|
||||
accepterId: locals.user.id
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return json({ success: true, friendship: updatedFriendship[0] });
|
||||
} catch (err) {
|
||||
console.error('Error updating friendship:', err);
|
||||
|
||||
77
src/routes/api/notifications/+server.ts
Normal file
77
src/routes/api/notifications/+server.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
import { notificationService } from '$lib/server/notifications';
|
||||
|
||||
// GET /api/notifications - Get user notifications
|
||||
export const GET: RequestHandler = async ({ locals, url }) => {
|
||||
const user = locals.user;
|
||||
if (!user) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const limit = parseInt(url.searchParams.get('limit') || '20');
|
||||
const offset = parseInt(url.searchParams.get('offset') || '0');
|
||||
const unreadOnly = url.searchParams.get('unreadOnly') === 'true';
|
||||
|
||||
try {
|
||||
const notifications = await notificationService.getUserNotifications(user.id, {
|
||||
limit,
|
||||
offset,
|
||||
unreadOnly
|
||||
});
|
||||
|
||||
return json({ notifications });
|
||||
} catch (error) {
|
||||
console.error('Error fetching notifications:', error);
|
||||
return json({ error: 'Failed to fetch notifications' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
// PATCH /api/notifications - Mark notifications as read
|
||||
export const PATCH: RequestHandler = async ({ locals, request }) => {
|
||||
const user = locals.user;
|
||||
if (!user) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { notificationIds, markAll } = await request.json();
|
||||
|
||||
if (markAll) {
|
||||
await notificationService.markAllAsRead(user.id);
|
||||
} else if (Array.isArray(notificationIds) && notificationIds.length > 0) {
|
||||
await notificationService.markAsRead(notificationIds);
|
||||
} else {
|
||||
return json({ error: 'Invalid request: provide notificationIds or markAll' }, { status: 400 });
|
||||
}
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error marking notifications as read:', error);
|
||||
return json({ error: 'Failed to mark notifications as read' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
// DELETE /api/notifications - Delete notifications
|
||||
export const DELETE: RequestHandler = async ({ locals, request }) => {
|
||||
const user = locals.user;
|
||||
if (!user) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { notificationId, deleteAll } = await request.json();
|
||||
|
||||
if (deleteAll) {
|
||||
await notificationService.deleteAllNotifications(user.id);
|
||||
} else if (notificationId) {
|
||||
await notificationService.deleteNotification(notificationId, user.id);
|
||||
} else {
|
||||
return json({ error: 'Invalid request: provide notificationId or deleteAll' }, { status: 400 });
|
||||
}
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error deleting notifications:', error);
|
||||
return json({ error: 'Failed to delete notifications' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
18
src/routes/api/notifications/count/+server.ts
Normal file
18
src/routes/api/notifications/count/+server.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
import { notificationService } from '$lib/server/notifications';
|
||||
|
||||
// GET /api/notifications/count - Get unread notification count
|
||||
export const GET: RequestHandler = async ({ locals }) => {
|
||||
const user = locals.user;
|
||||
if (!user) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const count = await notificationService.getUnreadCount(user.id);
|
||||
return json({ count });
|
||||
} catch (error) {
|
||||
console.error('Error fetching unread count:', error);
|
||||
return json({ error: 'Failed to fetch unread count' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
54
src/routes/api/notifications/subscribe/+server.ts
Normal file
54
src/routes/api/notifications/subscribe/+server.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
import { pushService, VAPID_PUBLIC_KEY } from '$lib/server/push';
|
||||
|
||||
// GET /api/notifications/subscribe - Get VAPID public key
|
||||
export const GET: RequestHandler = async () => {
|
||||
return json({ publicKey: VAPID_PUBLIC_KEY });
|
||||
};
|
||||
|
||||
// POST /api/notifications/subscribe - Subscribe to push notifications
|
||||
export const POST: RequestHandler = async ({ locals, request }) => {
|
||||
const user = locals.user;
|
||||
if (!user) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { subscription } = await request.json();
|
||||
const userAgent = request.headers.get('user-agent') || undefined;
|
||||
|
||||
if (!subscription || !subscription.endpoint || !subscription.keys) {
|
||||
return json({ error: 'Invalid subscription data' }, { status: 400 });
|
||||
}
|
||||
|
||||
await pushService.saveSubscription(user.id, subscription, userAgent);
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error saving subscription:', error);
|
||||
return json({ error: 'Failed to save subscription' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
// DELETE /api/notifications/subscribe - Unsubscribe from push notifications
|
||||
export const DELETE: RequestHandler = async ({ locals, request }) => {
|
||||
const user = locals.user;
|
||||
if (!user) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { endpoint } = await request.json();
|
||||
|
||||
if (!endpoint) {
|
||||
return json({ error: 'Endpoint is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
await pushService.removeSubscription(user.id, endpoint);
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error removing subscription:', error);
|
||||
return json({ error: 'Failed to remove subscription' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user