-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Add anonymous view notifications for non-logged-in viewers #1643
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 14 commits
cad016b
fbaa359
d47a072
7d7bfc4
a11e2da
b1d7d19
8d8229f
2ca2f56
93a7d14
83031ad
4962452
9f35e61
530e728
04dd97d
c1f59ee
57a49e9
cfccb35
f4d6cff
e77a15b
9136bb5
ec6eef3
d10e157
2a1e0df
8eae09f
b804dc7
0e406c5
a75b9ec
4c9d904
0c87ef5
915e508
509b9f5
d141666
ced5ce2
aa2fb75
ed0ce98
ff31881
a4d1625
935d152
d5674e0
9da8943
ee9a373
883bbf8
cb10822
4a03a8a
4d3c1c7
a734be2
6825367
9e854c9
3c69bca
cb235a3
0db13cc
cf495ee
2481d2b
2bcf989
5dfd9f1
c6c1eb2
080c81f
4305c6a
5ba1611
f587ef1
f3530e7
c1ec572
0cb4ad4
b4eb2d6
c37ec53
0223f5e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -19,7 +19,7 @@ const descriptionMap: Record<NotificationType, string> = { | |||||||||||||||
| reply: `replied to your comment`, | ||||||||||||||||
| view: `viewed your video`, | ||||||||||||||||
| reaction: `reacted to your video`, | ||||||||||||||||
| // mention: `mentioned you in a comment`, | ||||||||||||||||
| anon_view: `viewed your video`, | ||||||||||||||||
| }; | ||||||||||||||||
|
|
||||||||||||||||
| export const NotificationItem = ({ | ||||||||||||||||
|
|
@@ -36,6 +36,11 @@ export const NotificationItem = ({ | |||||||||||||||
| } | ||||||||||||||||
| }; | ||||||||||||||||
|
|
||||||||||||||||
| const isAnonView = notification.type === "anon_view"; | ||||||||||||||||
| const displayName = isAnonView | ||||||||||||||||
|
||||||||||||||||
| const displayName = isAnonView | |
| const displayName = | |
| notification.type === "anon_view" ? notification.anonName : notification.author.name; |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider reusing SignedImageUrl here for consistent avatar styling (and to avoid baking a special-case glyph into the UI).
| <div className="relative flex-shrink-0 size-7 rounded-full bg-gray-3 flex items-center justify-center text-sm"> | |
| <SignedImageUrl | |
| image={null} | |
| name={notification.anonName} | |
| className="relative flex-shrink-0 size-7" | |
| letterClass="text-sm" | |
| /> |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,8 +4,44 @@ import { Effect, Option } from "effect"; | |||||||||||||||||||||||
| import type { NextRequest } from "next/server"; | ||||||||||||||||||||||||
| import UAParser from "ua-parser-js"; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| import { getAnonymousName } from "@/lib/anonymous-names"; | ||||||||||||||||||||||||
| import { createAnonymousViewNotification } from "@/lib/Notification"; | ||||||||||||||||||||||||
| import { runPromise } from "@/lib/server"; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const anonNotifRateLimit = new Map< | ||||||||||||||||||||||||
| string, | ||||||||||||||||||||||||
| { count: number; resetAt: number } | ||||||||||||||||||||||||
| >(); | ||||||||||||||||||||||||
| const ANON_NOTIF_WINDOW_MS = 5 * 60 * 1000; | ||||||||||||||||||||||||
| const ANON_NOTIF_MAX_PER_VIDEO = 50; | ||||||||||||||||||||||||
| const ANON_NOTIF_MAX_ENTRIES = 10_000; | ||||||||||||||||||||||||
| let anonNotifCleanupCounter = 0; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| function checkAnonNotifRateLimit(videoId: string): boolean { | ||||||||||||||||||||||||
| anonNotifCleanupCounter++; | ||||||||||||||||||||||||
| if (anonNotifCleanupCounter % 100 === 0) { | ||||||||||||||||||||||||
| const now = Date.now(); | ||||||||||||||||||||||||
| for (const [k, v] of anonNotifRateLimit) { | ||||||||||||||||||||||||
| if (v.resetAt < now) anonNotifRateLimit.delete(k); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| if (anonNotifRateLimit.size > ANON_NOTIF_MAX_ENTRIES) | ||||||||||||||||||||||||
| anonNotifRateLimit.clear(); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const now = Date.now(); | ||||||||||||||||||||||||
| const entry = anonNotifRateLimit.get(videoId); | ||||||||||||||||||||||||
| if (!entry || entry.resetAt < now) { | ||||||||||||||||||||||||
| anonNotifRateLimit.set(videoId, { | ||||||||||||||||||||||||
| count: 1, | ||||||||||||||||||||||||
| resetAt: now + ANON_NOTIF_WINDOW_MS, | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| if (entry.count >= ANON_NOTIF_MAX_PER_VIDEO) return false; | ||||||||||||||||||||||||
| entry.count++; | ||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| interface TrackPayload { | ||||||||||||||||||||||||
| videoId: string; | ||||||||||||||||||||||||
| orgId?: string | null; | ||||||||||||||||||||||||
|
|
@@ -100,6 +136,34 @@ export async function POST(request: NextRequest) { | |||||||||||||||||||||||
| user_id: userId, | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||
| !userId && | ||||||||||||||||||||||||
| sessionId !== "anon" && | ||||||||||||||||||||||||
| checkAnonNotifRateLimit(body.videoId) | ||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
| if ( | |
| !userId && | |
| sessionId !== "anon" && | |
| checkAnonNotifRateLimit(body.videoId) | |
| ) { | |
| if ( | |
| !userId && | |
| sessionId.trim() !== "" && | |
| sessionId !== "anon" && | |
| checkAnonNotifRateLimit(body.videoId) | |
| ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
anon_viewlabel inFilterLabelsis unreachable UIanon_viewis not present in theFiltersarray (lines 5–11), so this entry inFilterLabelsis only there for TypeScript type completeness (becauseFilterTypeincludesanon_view). This is not a bug, but a comment explaining why the entry exists would help future readers understand it is intentional and not dead code that can be removed:Prompt To Fix With AI