From 86c51a40b381213516675a1971c92a7b3952e5c0 Mon Sep 17 00:00:00 2001 From: chev Date: Thu, 2 Jul 2026 13:49:15 -0500 Subject: [PATCH 1/2] feat: add deduped view notification lookup --- src/db/notifications.ts | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/db/notifications.ts b/src/db/notifications.ts index 8e873f5..7b46ccf 100644 --- a/src/db/notifications.ts +++ b/src/db/notifications.ts @@ -1,6 +1,6 @@ import 'server-only'; -import { and, desc, eq } from 'drizzle-orm'; +import { and, desc, eq, gte } from 'drizzle-orm'; import { db } from './client'; import { type NewNotificationRecord, @@ -58,6 +58,29 @@ export const createNotification = async (values: NewNotificationRecord) => { return created; }; +export const hasRecentViewNotification = async ( + userId: string, + actorName: string | null, + windowStart: Date, +): Promise => { + const [existing] = await db + .select({ id: notifications.id }) + .from(notifications) + .where( + and( + eq(notifications.userId, userId), + eq(notifications.kind, 'view'), + actorName === null + ? eq(notifications.isGuest, true) + : eq(notifications.actorName, actorName), + gte(notifications.createdAt, windowStart), + ), + ) + .limit(1); + + return existing !== undefined; +}; + export const setNotificationRead = async ( userId: string, notificationId: string, From 8381211f3d0e220a3e6c769db57354bfda7620a9 Mon Sep 17 00:00:00 2001 From: chev Date: Thu, 2 Jul 2026 13:49:25 -0500 Subject: [PATCH 2/2] feat: emit view notification on public profile view --- src/app/[lang]/u/[id]/page.tsx | 13 +++++++++ src/lib/notifications/profileView.ts | 41 ++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 src/lib/notifications/profileView.ts diff --git a/src/app/[lang]/u/[id]/page.tsx b/src/app/[lang]/u/[id]/page.tsx index 032ff6c..c3f5b14 100644 --- a/src/app/[lang]/u/[id]/page.tsx +++ b/src/app/[lang]/u/[id]/page.tsx @@ -10,6 +10,7 @@ import { import { ANALYTICS_EVENTS } from '@/lib/analytics/events'; import { trackEvent } from '@/lib/analytics/track.server'; import { getCurrentUser } from '@/lib/auth'; +import { notifyProfileView } from '@/lib/notifications/profileView'; import { PublicProfileClient } from './PublicProfileClient'; export default async function PublicProfileRoute({ @@ -32,11 +33,16 @@ export default async function PublicProfileRoute({ let currentProfileId: string | undefined; let viewerTimezone: string | undefined; let viewerUserId: string | undefined; + let viewerActor: { name: string; avatarUrl: string | null } | null = null; if (user) { isLoggedIn = true; const persistedUser = await upsertDiscordUser(user); viewerUserId = persistedUser.id; + viewerActor = { + name: persistedUser.displayName, + avatarUrl: persistedUser.avatarUrl, + }; const viewerProfile = await getProfileByUserId(persistedUser.id); currentProfileId = viewerProfile?.profile.id; savedProfileIds = await listSavedProfileIds(persistedUser.id); @@ -54,6 +60,13 @@ export default async function PublicProfileRoute({ locale: lang, }); + if (viewerUserId !== row.profile.userId) { + await notifyProfileView({ + ownerUserId: row.profile.userId, + actor: viewerActor, + }); + } + return ( => { + try { + const windowStart = new Date(Date.now() - VIEW_DEDUPE_WINDOW_MS); + const duplicate = await hasRecentViewNotification( + ownerUserId, + actor?.name ?? null, + windowStart, + ); + + if (duplicate) { + return; + } + + await createNotification({ + userId: ownerUserId, + kind: 'view', + actorName: actor?.name ?? null, + actorAvatarUrl: actor?.avatarUrl ?? null, + isGuest: actor === null, + }); + } catch (error) { + console.error('profile view notification failed', error); + } +};