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..c76ca2ba6f3 --- /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("🙌", { id: "user-123" }); + }); +}); diff --git a/apps/web/app/s/[videoId]/_components/Toolbar.tsx b/apps/web/app/s/[videoId]/_components/Toolbar.tsx index 84a9ddb5c16..64420f05d49 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, authedUser) => { + 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: authedUser.id, + authorName: authedUser.name, + authorImage: authedUser.imageUrl, + 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: authedUser.imageUrl, + 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..99a1ff3791b --- /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: TUser | null | undefined; + onRequireAuth: () => void; + onReact: (emoji: string, user: TUser) => void | Promise; +} + +export function handleEmojiReaction({ + emoji, + canReact, + user, + onRequireAuth, + onReact, +}: HandleEmojiReactionParams): void { + if (!canReact) return; + if (!user) { + onRequireAuth(); + return; + } + void onReact(emoji, user); +}