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 ( { 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, diff --git a/src/lib/notifications/profileView.ts b/src/lib/notifications/profileView.ts new file mode 100644 index 0000000..909efee --- /dev/null +++ b/src/lib/notifications/profileView.ts @@ -0,0 +1,41 @@ +import 'server-only'; + +import { createNotification, hasRecentViewNotification } from '@/db'; + +const VIEW_DEDUPE_WINDOW_MS = 60 * 60 * 1000; + +type ProfileViewActor = { + name: string; + avatarUrl: string | null; +}; + +export const notifyProfileView = async ({ + ownerUserId, + actor, +}: { + ownerUserId: string; + actor: ProfileViewActor | null; +}): Promise => { + 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); + } +};