diff --git a/docs/notifications.md b/docs/notifications.md index 19304a1..72590ec 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -11,11 +11,14 @@ The in-app activity feed shows a chronological list of notifications via the `Ac All notification types are stored in the `notifications` database table with read/unread tracking. The bottom navigation shows an unread badge count. Users can dismiss individual notifications via the `DELETE /api/notifications/[id]` endpoint. -### Auto-Clear Behavior +### Read / Dismiss Behavior -Certain user actions automatically delete related notifications to keep the activity feed clean: -- **Watching a clip** deletes any `new_clip` notification for that clip -- **Viewing comments** on a clip deletes `comment`, `reply`, and `mention` notifications for that clip +Notifications persist in the activity feed until the user explicitly dismisses them with the `X` button. Opening the activity feed or interacting with a clip only marks notifications as **read** (removing the unread tint and badge); the rows remain visible: +- **Opening the activity feed** marks all unread notifications as read. +- **Watching a clip** marks that clip's `reaction` notifications as read (after a 3s dwell). +- **Viewing comments** on a clip marks that clip's `comment`, `reply`, and `mention` notifications as read. + +Manual deletion (single-row `DELETE /api/notifications/[id]`) is the only path that removes a notification from the feed. ### API Endpoints diff --git a/src/lib/components/ReelItem.svelte b/src/lib/components/ReelItem.svelte index 19434db..2e3a280 100644 --- a/src/lib/components/ReelItem.svelte +++ b/src/lib/components/ReelItem.svelte @@ -250,7 +250,7 @@ fetch('/api/notifications/mark-read', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ clipId: clip.id, type: 'reaction', delete: true }) + body: JSON.stringify({ clipId: clip.id, type: 'reaction' }) }) .then(() => fetchUnreadCount()) .catch(() => {}); diff --git a/src/routes/api/clips/[id]/comments/viewed/+server.ts b/src/routes/api/clips/[id]/comments/viewed/+server.ts index 6f6e63f..5ff6233 100644 --- a/src/routes/api/clips/[id]/comments/viewed/+server.ts +++ b/src/routes/api/clips/[id]/comments/viewed/+server.ts @@ -2,7 +2,7 @@ import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { db } from '$lib/server/db'; import { commentViews, notifications } from '$lib/server/db/schema'; -import { eq, and, inArray } from 'drizzle-orm'; +import { eq, and, inArray, isNull } from 'drizzle-orm'; import { withClipAuth } from '$lib/server/api-utils'; export const POST: RequestHandler = withClipAuth(async ({ params }, { user }) => { @@ -24,14 +24,17 @@ export const POST: RequestHandler = withClipAuth(async ({ params }, { user }) => await db.insert(commentViews).values({ clipId, userId, viewedAt: now }); } - // Auto-clear comment/reply/mention notifications now that the user has viewed comments + // Mark comment/reply/mention notifications as read (but keep them in the + // activity feed — users dismiss them manually with the X button). await db - .delete(notifications) + .update(notifications) + .set({ readAt: now }) .where( and( eq(notifications.userId, userId), eq(notifications.clipId, clipId), - inArray(notifications.type, ['comment', 'reply', 'mention']) + inArray(notifications.type, ['comment', 'reply', 'mention']), + isNull(notifications.readAt) ) ); diff --git a/src/routes/api/clips/[id]/favorite/+server.ts b/src/routes/api/clips/[id]/favorite/+server.ts index c7ef89e..cdc16a1 100644 --- a/src/routes/api/clips/[id]/favorite/+server.ts +++ b/src/routes/api/clips/[id]/favorite/+server.ts @@ -62,15 +62,17 @@ export const POST: RequestHandler = withClipAuth(async ({ params }, { user, clip }) .run(); - // Also create ❤️ reaction if one doesn't already exist - const existingReaction = tx + // Only create a paired ❤️ reaction if this user has NO reaction yet. + // If they already reacted with any emoji (e.g. 🔥), preserve their + // chosen reaction — the favorite saves the clip without overwriting + // what they expressed. This also keeps the "one reaction per user + // per clip" invariant the comments view relies on. + const existingAnyReaction = tx .select() .from(reactions) - .where( - and(eq(reactions.clipId, clipId), eq(reactions.userId, userId), eq(reactions.emoji, '❤️')) - ) + .where(and(eq(reactions.clipId, clipId), eq(reactions.userId, userId))) .get(); - if (!existingReaction) { + if (!existingAnyReaction) { tx.insert(reactions) .values({ id: uuid(),