From 2be0ac86c23e6fc2007a26d8f7609c9a4815523a Mon Sep 17 00:00:00 2001 From: GhaZy Date: Fri, 23 Oct 2026 12:26:29 +0300 Subject: [PATCH 1/2] fix(web): prompt sign-in overlay when unauthenticated viewer reacts with emoji (#2976) --- .../__tests__/unit/toolbar-reactions.test.ts | 83 ++++++++++++ .../app/s/[videoId]/_components/Toolbar.tsx | 121 ++++++++---------- .../_components/toolbar-reactions.ts | 54 ++++++++ 3 files changed, 189 insertions(+), 69 deletions(-) create mode 100644 apps/web/__tests__/unit/toolbar-reactions.test.ts create mode 100644 apps/web/app/s/[videoId]/_components/toolbar-reactions.ts diff --git a/apps/web/__tests__/unit/toolbar-reactions.test.ts b/apps/web/__tests__/unit/toolbar-reactions.test.ts new file mode 100644 index 00000000000..1281e29ee04 --- /dev/null +++ b/apps/web/__tests__/unit/toolbar-reactions.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it, vi } from "vitest"; +import { + handleEmojiReaction, + REACTIONS, +} from "@/app/s/[videoId]/_components/toolbar-reactions"; + +describe("toolbar reactions", () => { + it("exposes the standard reaction set in display order", () => { + expect(REACTIONS).toEqual([ + { emoji: "😂", label: "joy" }, + { emoji: "😍", label: "love" }, + { emoji: "😮", label: "wow" }, + { emoji: "🙌", label: "yay" }, + { emoji: "👍", label: "up" }, + { emoji: "👎", label: "down" }, + ]); + }); + + it("prompts sign-in overlay when unauthenticated viewer reacts", () => { + const onRequireAuth = vi.fn(); + const onReact = vi.fn(); + + handleEmojiReaction({ + emoji: "😂", + canReact: true, + user: null, + onRequireAuth, + onReact, + }); + + expect(onRequireAuth).toHaveBeenCalledOnce(); + expect(onReact).not.toHaveBeenCalled(); + }); + + it("prompts sign-in overlay when user is undefined", () => { + const onRequireAuth = vi.fn(); + const onReact = vi.fn(); + + handleEmojiReaction({ + emoji: "👍", + canReact: true, + user: undefined, + onRequireAuth, + onReact, + }); + + expect(onRequireAuth).toHaveBeenCalledOnce(); + expect(onReact).not.toHaveBeenCalled(); + }); + + it("suppresses interaction when reactions are disabled", () => { + const onRequireAuth = vi.fn(); + const onReact = vi.fn(); + + handleEmojiReaction({ + emoji: "😍", + canReact: false, + user: null, + onRequireAuth, + onReact, + }); + + expect(onRequireAuth).not.toHaveBeenCalled(); + expect(onReact).not.toHaveBeenCalled(); + }); + + it("submits reaction when viewer is signed in", () => { + const onRequireAuth = vi.fn(); + const onReact = vi.fn(); + + handleEmojiReaction({ + emoji: "🙌", + canReact: true, + user: { id: "user-123" }, + onRequireAuth, + onReact, + }); + + expect(onRequireAuth).not.toHaveBeenCalled(); + expect(onReact).toHaveBeenCalledOnce(); + expect(onReact).toHaveBeenCalledWith("🙌"); + }); +}); diff --git a/apps/web/app/s/[videoId]/_components/Toolbar.tsx b/apps/web/app/s/[videoId]/_components/Toolbar.tsx index 84a9ddb5c16..47a903a1619 100644 --- a/apps/web/app/s/[videoId]/_components/Toolbar.tsx +++ b/apps/web/app/s/[videoId]/_components/Toolbar.tsx @@ -13,6 +13,7 @@ import type { VideoData } from "../types"; import { AuthOverlay } from "./AuthOverlayLazy"; import { RecordActionButtons } from "./media-comment/record-actions"; import type { RecordIntentKind } from "./timeline/TimelineComposer"; +import { handleEmojiReaction, REACTIONS } from "./toolbar-reactions"; // Capture and upload code stays out of the page bundle until someone records. const RecorderSurface = dynamic( @@ -84,50 +85,59 @@ export const Toolbar = ({ setRecordIntent({ kind, t }); }; - const handleEmojiClick = async (emoji: string) => { - if (!canReact || !user) return; - const videoElement = document.querySelector("video") as HTMLVideoElement; - const currentTime = data.isScreenshot - ? null - : videoElement?.currentTime || 0; - const optimisticComment: CommentType = { - id: `temp-${Date.now()}` as Comment.CommentId, - authorId: user.id, - authorName: user.name, - authorImage: user.imageUrl, - content: emoji, - createdAt: new Date(), - videoId: data.id, - parentCommentId: "" as Comment.CommentId, - type: "emoji", - timestamp: currentTime, - updatedAt: new Date(), - mediaKey: null, - mediaDuration: null, - mediaMeta: null, - sending: true, - }; + const handleEmojiClick = (emoji: string) => { + handleEmojiReaction({ + emoji, + canReact, + user, + onRequireAuth: () => setShowAuthOverlay(true), + onReact: async (chosenEmoji) => { + const videoElement = document.querySelector( + "video", + ) as HTMLVideoElement; + const currentTime = data.isScreenshot + ? null + : videoElement?.currentTime || 0; + const optimisticComment: CommentType = { + id: `temp-${Date.now()}` as Comment.CommentId, + authorId: user?.id ?? "", + authorName: user?.name ?? null, + authorImage: user?.imageUrl ?? null, + content: chosenEmoji, + createdAt: new Date(), + videoId: data.id, + parentCommentId: "" as Comment.CommentId, + type: "emoji", + timestamp: currentTime, + updatedAt: new Date(), + mediaKey: null, + mediaDuration: null, + mediaMeta: null, + sending: true, + }; - onOptimisticComment?.(optimisticComment); + onOptimisticComment?.(optimisticComment); - try { - const newCommentData = await newComment({ - content: emoji, - videoId: data.id, - authorImage: user.imageUrl, - parentCommentId: "" as Comment.CommentId, - type: "emoji", - timestamp: currentTime, - }); - startTransition(() => { - onCommentSuccess?.(newCommentData); - }); - } catch (error) { - console.error("Error posting comment:", error); - } finally { - setCommentBoxOpen(false); - setComment(""); - } + try { + const newCommentData = await newComment({ + content: chosenEmoji, + videoId: data.id, + authorImage: user?.imageUrl ?? null, + parentCommentId: "" as Comment.CommentId, + type: "emoji", + timestamp: currentTime, + }); + startTransition(() => { + onCommentSuccess?.(newCommentData); + }); + } catch (error) { + console.error("Error posting comment:", error); + } finally { + setCommentBoxOpen(false); + setComment(""); + } + }, + }); }; const handleCommentSubmit = async () => { @@ -377,30 +387,3 @@ export const Toolbar = ({ ); }; - -const REACTIONS = [ - { - emoji: "😂", - label: "joy", - }, - { - emoji: "😍", - label: "love", - }, - { - emoji: "😮", - label: "wow", - }, - { - emoji: "🙌", - label: "yay", - }, - { - emoji: "👍", - label: "up", - }, - { - emoji: "👎", - label: "down", - }, -]; diff --git a/apps/web/app/s/[videoId]/_components/toolbar-reactions.ts b/apps/web/app/s/[videoId]/_components/toolbar-reactions.ts new file mode 100644 index 00000000000..666da216d14 --- /dev/null +++ b/apps/web/app/s/[videoId]/_components/toolbar-reactions.ts @@ -0,0 +1,54 @@ +export interface ReactionDefinition { + emoji: string; + label: string; +} + +export const REACTIONS: readonly ReactionDefinition[] = [ + { + emoji: "😂", + label: "joy", + }, + { + emoji: "😍", + label: "love", + }, + { + emoji: "😮", + label: "wow", + }, + { + emoji: "🙌", + label: "yay", + }, + { + emoji: "👍", + label: "up", + }, + { + emoji: "👎", + label: "down", + }, +]; + +export interface HandleEmojiReactionParams { + emoji: string; + canReact: boolean; + user: { id: string } | null | undefined; + onRequireAuth: () => void; + onReact: (emoji: string) => void | Promise; +} + +export function handleEmojiReaction({ + emoji, + canReact, + user, + onRequireAuth, + onReact, +}: HandleEmojiReactionParams): void { + if (!canReact) return; + if (!user) { + onRequireAuth(); + return; + } + void onReact(emoji); +} From 674dc511bef8f7ddbdf537fee0ad4124b6efc679 Mon Sep 17 00:00:00 2001 From: GhaZy Date: Fri, 23 Oct 2026 17:21:40 +0300 Subject: [PATCH 2/2] refactor(web): pass narrowed authenticated user to onReact in toolbar reactions --- apps/web/__tests__/unit/toolbar-reactions.test.ts | 2 +- apps/web/app/s/[videoId]/_components/Toolbar.tsx | 10 +++++----- .../app/s/[videoId]/_components/toolbar-reactions.ts | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/web/__tests__/unit/toolbar-reactions.test.ts b/apps/web/__tests__/unit/toolbar-reactions.test.ts index 1281e29ee04..c76ca2ba6f3 100644 --- a/apps/web/__tests__/unit/toolbar-reactions.test.ts +++ b/apps/web/__tests__/unit/toolbar-reactions.test.ts @@ -78,6 +78,6 @@ describe("toolbar reactions", () => { expect(onRequireAuth).not.toHaveBeenCalled(); expect(onReact).toHaveBeenCalledOnce(); - expect(onReact).toHaveBeenCalledWith("🙌"); + expect(onReact).toHaveBeenCalledWith("🙌", { id: "user-123" }); }); }); diff --git a/apps/web/app/s/[videoId]/_components/Toolbar.tsx b/apps/web/app/s/[videoId]/_components/Toolbar.tsx index 47a903a1619..64420f05d49 100644 --- a/apps/web/app/s/[videoId]/_components/Toolbar.tsx +++ b/apps/web/app/s/[videoId]/_components/Toolbar.tsx @@ -91,7 +91,7 @@ export const Toolbar = ({ canReact, user, onRequireAuth: () => setShowAuthOverlay(true), - onReact: async (chosenEmoji) => { + onReact: async (chosenEmoji, authedUser) => { const videoElement = document.querySelector( "video", ) as HTMLVideoElement; @@ -100,9 +100,9 @@ export const Toolbar = ({ : videoElement?.currentTime || 0; const optimisticComment: CommentType = { id: `temp-${Date.now()}` as Comment.CommentId, - authorId: user?.id ?? "", - authorName: user?.name ?? null, - authorImage: user?.imageUrl ?? null, + authorId: authedUser.id, + authorName: authedUser.name, + authorImage: authedUser.imageUrl, content: chosenEmoji, createdAt: new Date(), videoId: data.id, @@ -122,7 +122,7 @@ export const Toolbar = ({ const newCommentData = await newComment({ content: chosenEmoji, videoId: data.id, - authorImage: user?.imageUrl ?? null, + authorImage: authedUser.imageUrl, parentCommentId: "" as Comment.CommentId, type: "emoji", timestamp: currentTime, diff --git a/apps/web/app/s/[videoId]/_components/toolbar-reactions.ts b/apps/web/app/s/[videoId]/_components/toolbar-reactions.ts index 666da216d14..99a1ff3791b 100644 --- a/apps/web/app/s/[videoId]/_components/toolbar-reactions.ts +++ b/apps/web/app/s/[videoId]/_components/toolbar-reactions.ts @@ -30,25 +30,25 @@ export const REACTIONS: readonly ReactionDefinition[] = [ }, ]; -export interface HandleEmojiReactionParams { +export interface HandleEmojiReactionParams { emoji: string; canReact: boolean; - user: { id: string } | null | undefined; + user: TUser | null | undefined; onRequireAuth: () => void; - onReact: (emoji: string) => void | Promise; + onReact: (emoji: string, user: TUser) => void | Promise; } -export function handleEmojiReaction({ +export function handleEmojiReaction({ emoji, canReact, user, onRequireAuth, onReact, -}: HandleEmojiReactionParams): void { +}: HandleEmojiReactionParams): void { if (!canReact) return; if (!user) { onRequireAuth(); return; } - void onReact(emoji); + void onReact(emoji, user); }