From 44c596323582fe5852f8014837d737e2f14dd481 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Sat, 27 Sep 2025 13:27:42 +0300 Subject: [PATCH 01/15] comment stamps --- apps/web/actions/videos/new-comment.ts | 4 +- .../dashboard/_components/Navbar/Items.tsx | 22 ++--- apps/web/app/s/[videoId]/Share.tsx | 5 +- .../[videoId]/_components/CapVideoPlayer.tsx | 89 +++++++++++++++++-- .../s/[videoId]/_components/ShareVideo.tsx | 33 ++++++- .../_components/tabs/Activity/Comment.tsx | 39 ++++---- .../_components/tabs/Activity/Comments.tsx | 19 ++-- .../_components/tabs/Activity/index.tsx | 1 + 8 files changed, 162 insertions(+), 50 deletions(-) diff --git a/apps/web/actions/videos/new-comment.ts b/apps/web/actions/videos/new-comment.ts index b20d6128c2..ec588c7d87 100644 --- a/apps/web/actions/videos/new-comment.ts +++ b/apps/web/actions/videos/new-comment.ts @@ -13,6 +13,7 @@ export async function newComment(data: { videoId: Video.VideoId; type: "text" | "emoji"; parentCommentId: string; + timestamp?: number; }) { const user = await getCurrentUser(); @@ -24,6 +25,7 @@ export async function newComment(data: { const videoId = data.videoId; const type = data.type; const parentCommentId = data.parentCommentId; + const timestamp = data.timestamp; const conditionalType = parentCommentId ? "reply" : type === "emoji" @@ -41,7 +43,7 @@ export async function newComment(data: { type: type, content: content, videoId: videoId, - timestamp: null, + timestamp: timestamp || null, parentCommentId: parentCommentId, createdAt: new Date(), updatedAt: new Date(), diff --git a/apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx b/apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx index b17c9ac84e..eaa8988bb6 100644 --- a/apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx +++ b/apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx @@ -92,6 +92,9 @@ const AdminNavItems = ({ toggleMobileNav }: Props) => { const router = useRouter(); const isPathActive = (path: string) => pathname.includes(path); + const isDomainSetupVerified = + activeOrg?.organization.customDomain && + activeOrg?.organization.domainVerified; return ( @@ -179,33 +182,24 @@ const AdminNavItems = ({ toggleMobileNav }: Props) => { {!sidebarCollapsed && (

- {activeOrg?.organization.customDomain && - activeOrg?.organization.domainVerified + {isDomainSetupVerified ? activeOrg?.organization.customDomain : "No custom domain set"}

diff --git a/apps/web/app/s/[videoId]/Share.tsx b/apps/web/app/s/[videoId]/Share.tsx index 28602e4217..150a72ddf8 100644 --- a/apps/web/app/s/[videoId]/Share.tsx +++ b/apps/web/app/s/[videoId]/Share.tsx @@ -213,11 +213,11 @@ export const Share = ({ const aiLoading = shouldShowLoading(); - const handleSeek = (time: number) => { + const handleSeek = useCallback((time: number) => { if (playerRef.current) { playerRef.current.currentTime = time; } - }; + }, []); const handleOptimisticComment = useCallback( (comment: CommentType) => { @@ -250,6 +250,7 @@ export const Share = ({ comments={comments} chapters={aiData?.chapters || []} aiProcessing={aiData?.processing || false} + onSeek={handleSeek} ref={playerRef} /> diff --git a/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx b/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx index 9eae370b6a..0fe25db7b4 100644 --- a/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx +++ b/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx @@ -2,7 +2,7 @@ import { LogoSpinner } from "@cap/ui"; import type { Video } from "@cap/web-domain"; -import { faPlay } from "@fortawesome/free-solid-svg-icons"; +import { faComment, faPlay } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import clsx from "clsx"; import { AnimatePresence, motion } from "framer-motion"; @@ -39,6 +39,13 @@ interface Props { autoplay?: boolean; enableCrossOrigin?: boolean; hasActiveUpload: boolean | undefined; + comments?: Array<{ + id: string; + timestamp: number | null; + content: string; + authorName?: string | null; + }>; + onSeek?: (time: number) => void; } export function CapVideoPlayer({ @@ -51,6 +58,8 @@ export function CapVideoPlayer({ autoplay = false, enableCrossOrigin = false, hasActiveUpload, + comments = [], + onSeek, }: Props) { const [currentCue, setCurrentCue] = useState(""); const [controlsVisible, setControlsVisible] = useState(false); @@ -69,6 +78,7 @@ export function CapVideoPlayer({ const [isRetrying, setIsRetrying] = useState(false); const isRetryingRef = useRef(false); const maxRetries = 3; + const [duration, setDuration] = useState(0); useEffect(() => { const checkMobile = () => { @@ -218,6 +228,32 @@ export function CapVideoPlayer({ fetchNewUrl(); }, [fetchNewUrl]); + // Track video duration for comment markers + useEffect(() => { + const video = videoRef.current; + if (!video) return; + + const handleLoadedMetadata = () => { + setDuration(video.duration); + }; + + video.addEventListener("loadedmetadata", handleLoadedMetadata); + + return () => { + video.removeEventListener("loadedmetadata", handleLoadedMetadata); + }; + }, [urlResolved]); + + // Track when all data is ready for comment markers + const [markersReady, setMarkersReady] = useState(false); + + useEffect(() => { + // Only show markers when we have duration, comments, and video element + if (duration > 0 && comments.length > 0 && videoRef.current) { + setMarkersReady(true); + } + }, [duration, comments.length]); + useEffect(() => { const video = videoRef.current; if (!video || !urlResolved) return; @@ -484,13 +520,15 @@ export function CapVideoPlayer({ playsInline autoPlay={autoplay} > - - + {chaptersSrc && } + {captionsSrc && ( + + )} )} @@ -551,6 +589,41 @@ export function CapVideoPlayer({ )} + + {markersReady && + comments + .filter((comment) => comment.timestamp !== null) + .map((comment) => { + const position = (Number(comment.timestamp) / duration) * 100; + const containerPadding = 20; + const availableWidth = `calc(100% - ${containerPadding * 2}px)`; + const adjustedPosition = `calc(${containerPadding}px + (${position}% * ${availableWidth} / 100%))`; + + return ( + + ); + })} + ; chapters?: { title: string; start: number }[]; aiProcessing?: boolean; + onSeek?: (time: number) => void; } ->(({ data, user, comments, chapters = [], aiProcessing = false }, ref) => { +>(({ data, comments, chapters = [], onSeek }, ref) => { const videoRef = useRef(null); useImperativeHandle(ref, () => videoRef.current as HTMLVideoElement); @@ -50,12 +51,31 @@ export const ShareVideo = forwardRef< const [transcriptData, setTranscriptData] = useState([]); const [subtitleUrl, setSubtitleUrl] = useState(null); const [chaptersUrl, setChaptersUrl] = useState(null); + const [commentsData, setCommentsData] = useState([]); const { data: transcriptContent, error: transcriptError } = useTranscript( data.id, data.transcriptionStatus, ); + // Handle comments data + useEffect(() => { + if (comments) { + if (Array.isArray(comments)) { + setCommentsData(comments); + } else { + comments.then(setCommentsData); + } + } + }, [comments]); + + // Handle seek functionality + const handleSeek = (time: number) => { + if (videoRef.current) { + videoRef.current.currentTime = time; + } + }; + useEffect(() => { if (transcriptContent) { const parsed = parseVTT(transcriptContent); @@ -96,7 +116,7 @@ export const ShareVideo = forwardRef< setSubtitleUrl(null); } } - }, [data.transcriptionStatus, transcriptData]); + }, [data.transcriptionStatus, transcriptData, subtitleUrl]); // Handle chapters URL creation useEffect(() => { @@ -122,7 +142,7 @@ export const ShareVideo = forwardRef< setChaptersUrl(null); } } - }, [chapters]); + }, [chapters, chaptersUrl]); let videoSrc: string; let enableCrossOrigin = false; @@ -156,6 +176,13 @@ export const ShareVideo = forwardRef< videoRef={videoRef} enableCrossOrigin={enableCrossOrigin} hasActiveUpload={data.hasActiveUpload} + comments={commentsData.map((comment) => ({ + id: comment.id, + timestamp: comment.timestamp, + content: comment.content, + authorName: comment.authorName, + }))} + onSeek={handleSeek} /> ) : ( -
-

+

+

{comment.authorName || "Anonymous"}

- -

- {formatTimeAgo(commentDate)} -

-
- {comment.timestamp && ( - - )} +
+ +

+ {formatTimeAgo(commentDate)} +

+
+ {comment.timestamp !== null && ( + + )} +
-

{comment.content}

+

{comment.content}

{user && !isReplying && canReply && ( diff --git a/apps/web/app/s/[videoId]/_components/tabs/Activity/Comments.tsx b/apps/web/app/s/[videoId]/_components/tabs/Activity/Comments.tsx index 92337c8911..a9fb46c852 100644 --- a/apps/web/app/s/[videoId]/_components/tabs/Activity/Comments.tsx +++ b/apps/web/app/s/[videoId]/_components/tabs/Activity/Comments.tsx @@ -7,6 +7,7 @@ import { type ComponentProps, forwardRef, type PropsWithChildren, + useCallback, useEffect, useImperativeHandle, useRef, @@ -38,6 +39,7 @@ export const Comments = Object.assign( setOptimisticComments, setComments, handleCommentSuccess, + onSeek, } = props; const commentParams = useSearchParams().get("comment"); const replyParams = useSearchParams().get("reply"); @@ -53,24 +55,28 @@ export const Comments = Object.assign( commentsContainerRef.current.scrollTop = commentsContainerRef.current.scrollHeight; } - }, []); + }, [commentParams, replyParams]); - const scrollToBottom = () => { + const scrollToBottom = useCallback(() => { if (commentsContainerRef.current) { commentsContainerRef.current.scrollTo({ top: commentsContainerRef.current.scrollHeight, behavior: "smooth", }); } - }; + }, []); - useImperativeHandle(ref, () => ({ scrollToBottom }), []); + useImperativeHandle(ref, () => ({ scrollToBottom }), [scrollToBottom]); const rootComments = optimisticComments.filter( (comment) => !comment.parentCommentId || comment.parentCommentId === "", ); const handleNewComment = async (content: string) => { + // Get current video time from the video element + const videoElement = document.querySelector("video"); + const currentTime = videoElement?.currentTime || 0; + const optimisticComment: CommentType = { id: `temp-${Date.now()}`, authorId: user?.id || "anonymous", @@ -80,7 +86,7 @@ export const Comments = Object.assign( videoId: props.videoId, parentCommentId: "", type: "text", - timestamp: null, + timestamp: currentTime, updatedAt: new Date(), sending: true, }; @@ -93,6 +99,7 @@ export const Comments = Object.assign( videoId: props.videoId, parentCommentId: "", type: "text", + timestamp: currentTime, }); handleCommentSuccess(data); } catch (error) { @@ -195,7 +202,7 @@ export const Comments = Object.assign( onCancelReply={handleCancelReply} onDelete={handleDeleteComment} user={user} - onSeek={props.onSeek} + onSeek={onSeek} /> ))}
diff --git a/apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx b/apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx index bbc8f3671c..2d9a3ab77f 100644 --- a/apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx +++ b/apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx @@ -64,6 +64,7 @@ export const Activity = Object.assign( user={user} videoId={videoId} setShowAuthOverlay={setShowAuthOverlay} + onSeek={props.onSeek} /> )} From 4243077a07e3b85e827737467dee09f21183fcba Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Sat, 27 Sep 2025 13:47:49 +0300 Subject: [PATCH 02/15] reactions --- apps/web/actions/videos/new-comment.ts | 2 +- .../[videoId]/_components/CapVideoPlayer.tsx | 73 +++++++++++++++---- .../s/[videoId]/_components/ShareVideo.tsx | 1 + .../app/s/[videoId]/_components/Toolbar.tsx | 11 ++- .../_components/tabs/Activity/Comment.tsx | 2 +- 5 files changed, 68 insertions(+), 21 deletions(-) diff --git a/apps/web/actions/videos/new-comment.ts b/apps/web/actions/videos/new-comment.ts index ec588c7d87..82d6cc568c 100644 --- a/apps/web/actions/videos/new-comment.ts +++ b/apps/web/actions/videos/new-comment.ts @@ -13,7 +13,7 @@ export async function newComment(data: { videoId: Video.VideoId; type: "text" | "emoji"; parentCommentId: string; - timestamp?: number; + timestamp: number; }) { const user = await getCurrentUser(); diff --git a/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx b/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx index 0fe25db7b4..9116467ff8 100644 --- a/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx +++ b/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx @@ -1,8 +1,12 @@ "use client"; -import { LogoSpinner } from "@cap/ui"; +import { Avatar, LogoSpinner } from "@cap/ui"; import type { Video } from "@cap/web-domain"; -import { faComment, faPlay } from "@fortawesome/free-solid-svg-icons"; +import { + faChevronRight, + faComment, + faPlay, +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import clsx from "clsx"; import { AnimatePresence, motion } from "framer-motion"; @@ -42,6 +46,7 @@ interface Props { comments?: Array<{ id: string; timestamp: number | null; + type: "text" | "emoji"; content: string; authorName?: string | null; }>; @@ -246,6 +251,7 @@ export function CapVideoPlayer({ // Track when all data is ready for comment markers const [markersReady, setMarkersReady] = useState(false); + const [hoveredComment, setHoveredComment] = useState(null); useEffect(() => { // Only show markers when we have duration, comments, and video element @@ -600,27 +606,62 @@ export function CapVideoPlayer({ const adjustedPosition = `calc(${containerPadding}px + (${position}% * ${availableWidth} / 100%))`; return ( - + {/* Comment marker */} + + + {hoveredComment === comment.id && ( +
+ {/* Arrow pointing down to marker */} +
+ +
+ {/* User avatar/initial */} + + {/* Comment content */} +
+
+ {comment.authorName || "Anonymous"} +
+
+ {comment.content} +
+
+
+
+ )} +
); })} diff --git a/apps/web/app/s/[videoId]/_components/ShareVideo.tsx b/apps/web/app/s/[videoId]/_components/ShareVideo.tsx index c2541eb3bd..c3bfa7a34d 100644 --- a/apps/web/app/s/[videoId]/_components/ShareVideo.tsx +++ b/apps/web/app/s/[videoId]/_components/ShareVideo.tsx @@ -178,6 +178,7 @@ export const ShareVideo = forwardRef< hasActiveUpload={data.hasActiveUpload} comments={commentsData.map((comment) => ({ id: comment.id, + type: comment.type, timestamp: comment.timestamp, content: comment.content, authorName: comment.authorName, diff --git a/apps/web/app/s/[videoId]/_components/Toolbar.tsx b/apps/web/app/s/[videoId]/_components/Toolbar.tsx index 1f67514d29..dbb521c94b 100644 --- a/apps/web/app/s/[videoId]/_components/Toolbar.tsx +++ b/apps/web/app/s/[videoId]/_components/Toolbar.tsx @@ -54,6 +54,8 @@ export const Toolbar = ({ }; const handleEmojiClick = async (emoji: string) => { + const videoElement = document.querySelector("video"); + const currentTime = videoElement?.currentTime || 0; const optimisticComment: CommentType = { id: `temp-${Date.now()}`, authorId: user?.id || "anonymous", @@ -63,7 +65,7 @@ export const Toolbar = ({ videoId: data.id, parentCommentId: "", type: "emoji", - timestamp: null, + timestamp: currentTime, updatedAt: new Date(), sending: true, }; @@ -76,6 +78,7 @@ export const Toolbar = ({ videoId: data.id, parentCommentId: "", type: "emoji", + timestamp: currentTime, }); startTransition(() => { onCommentSuccess?.(newCommentData); @@ -92,7 +95,8 @@ export const Toolbar = ({ if (comment.length === 0) { return; } - + const videoElement = document.querySelector("video"); + const currentTime = videoElement?.currentTime || 0; const optimisticComment: CommentType = { id: `temp-${Date.now()}`, authorId: user?.id || "anonymous", @@ -102,7 +106,7 @@ export const Toolbar = ({ videoId: data.id, parentCommentId: "", type: "text", - timestamp: null, + timestamp: currentTime, updatedAt: new Date(), sending: true, }; @@ -115,6 +119,7 @@ export const Toolbar = ({ videoId: data.id, parentCommentId: "", type: "text", + timestamp: currentTime, }); startTransition(() => { onCommentSuccess?.(newCommentData); diff --git a/apps/web/app/s/[videoId]/_components/tabs/Activity/Comment.tsx b/apps/web/app/s/[videoId]/_components/tabs/Activity/Comment.tsx index c7ca6cdbad..1258bba108 100644 --- a/apps/web/app/s/[videoId]/_components/tabs/Activity/Comment.tsx +++ b/apps/web/app/s/[videoId]/_components/tabs/Activity/Comment.tsx @@ -98,7 +98,7 @@ const Comment: React.FC<{

{comment.authorName || "Anonymous"}

-
+

{formatTimeAgo(commentDate)} From d985c2fbbc60bed9c5efc47b95611a60d5e2874b Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Sat, 27 Sep 2025 13:51:12 +0300 Subject: [PATCH 03/15] Update Share.tsx --- apps/web/app/s/[videoId]/Share.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/app/s/[videoId]/Share.tsx b/apps/web/app/s/[videoId]/Share.tsx index 150a72ddf8..1bb75e9d01 100644 --- a/apps/web/app/s/[videoId]/Share.tsx +++ b/apps/web/app/s/[videoId]/Share.tsx @@ -221,7 +221,9 @@ export const Share = ({ const handleOptimisticComment = useCallback( (comment: CommentType) => { - setOptimisticComments(comment); + startTransition(() => { + setOptimisticComments(comment); + }); setTimeout(() => { activityRef.current?.scrollToBottom(); }, 100); From b6eee987ccd915a563bd3ccbac1620cb0bc118dc Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Sat, 27 Sep 2025 14:07:19 +0300 Subject: [PATCH 04/15] use playerref --- apps/web/app/s/[videoId]/Share.tsx | 3 +++ .../app/s/[videoId]/_components/Sidebar.tsx | 5 ++++- .../app/s/[videoId]/_components/Toolbar.tsx | 12 +++++++----- .../_components/tabs/Activity/Comment.tsx | 2 +- .../_components/tabs/Activity/Comments.tsx | 19 ++++++++++++++----- .../_components/tabs/Activity/index.tsx | 11 ++++++++++- 6 files changed, 39 insertions(+), 13 deletions(-) diff --git a/apps/web/app/s/[videoId]/Share.tsx b/apps/web/app/s/[videoId]/Share.tsx index 1bb75e9d01..8611d089e5 100644 --- a/apps/web/app/s/[videoId]/Share.tsx +++ b/apps/web/app/s/[videoId]/Share.tsx @@ -261,6 +261,7 @@ export const Share = ({ @@ -274,6 +275,7 @@ export const Share = ({ createdAt: effectiveDate, transcriptionStatus, }} + playerRef={playerRef} user={user} commentsData={commentsData} setCommentsData={setCommentsData} @@ -297,6 +299,7 @@ export const Share = ({ onCommentSuccess={handleCommentSuccess} data={data} user={user} + playerRef={playerRef} />

diff --git a/apps/web/app/s/[videoId]/_components/Sidebar.tsx b/apps/web/app/s/[videoId]/_components/Sidebar.tsx index bb806ff7bf..5e53349819 100644 --- a/apps/web/app/s/[videoId]/_components/Sidebar.tsx +++ b/apps/web/app/s/[videoId]/_components/Sidebar.tsx @@ -3,7 +3,7 @@ import type { comments as commentsSchema, videos } from "@cap/database/schema"; import { classNames } from "@cap/utils"; import type { Video } from "@cap/web-domain"; import { AnimatePresence, motion } from "framer-motion"; -import { forwardRef, Suspense, useState } from "react"; +import { forwardRef, type RefObject, Suspense, useState } from "react"; import { Activity } from "./tabs/Activity"; import { Settings } from "./tabs/Settings"; import { Summary } from "./tabs/Summary"; @@ -31,6 +31,7 @@ interface SidebarProps { views: MaybePromise; onSeek?: (time: number) => void; videoId: Video.VideoId; + playerRef: RefObject; aiData?: { title?: string | null; summary?: string | null; @@ -75,6 +76,7 @@ export const Sidebar = forwardRef<{ scrollToBottom: () => void }, SidebarProps>( handleCommentSuccess, setOptimisticComments, views, + playerRef, onSeek, videoId, aiData, @@ -133,6 +135,7 @@ export const Sidebar = forwardRef<{ scrollToBottom: () => void }, SidebarProps>( isOwnerOrMember={isOwnerOrMember} onSeek={onSeek} videoId={videoId} + playerRef={playerRef} /> ); diff --git a/apps/web/app/s/[videoId]/_components/Toolbar.tsx b/apps/web/app/s/[videoId]/_components/Toolbar.tsx index dbb521c94b..af0105938b 100644 --- a/apps/web/app/s/[videoId]/_components/Toolbar.tsx +++ b/apps/web/app/s/[videoId]/_components/Toolbar.tsx @@ -2,7 +2,7 @@ import type { userSelectProps } from "@cap/database/auth/session"; import type { videos } from "@cap/database/schema"; import { Button } from "@cap/ui"; import { AnimatePresence, motion } from "motion/react"; -import { startTransition, useEffect, useState } from "react"; +import { type RefObject, startTransition, useEffect, useState } from "react"; import { newComment } from "@/actions/videos/new-comment"; import type { CommentType } from "../Share"; import { AuthOverlay } from "./AuthOverlay"; @@ -15,6 +15,7 @@ interface ToolbarProps { user: typeof userSelectProps | null; onOptimisticComment?: (comment: CommentType) => void; onCommentSuccess?: (comment: CommentType) => void; + playerRef: RefObject; } export const Toolbar = ({ @@ -22,6 +23,7 @@ export const Toolbar = ({ user, onOptimisticComment, onCommentSuccess, + playerRef, }: ToolbarProps) => { const [commentBoxOpen, setCommentBoxOpen] = useState(false); const [comment, setComment] = useState(""); @@ -54,8 +56,9 @@ export const Toolbar = ({ }; const handleEmojiClick = async (emoji: string) => { - const videoElement = document.querySelector("video"); - const currentTime = videoElement?.currentTime || 0; + const currentTime = playerRef?.current?.currentTime || 0; + console.log("playerRef", playerRef); + console.log("currentTime", currentTime); const optimisticComment: CommentType = { id: `temp-${Date.now()}`, authorId: user?.id || "anonymous", @@ -95,8 +98,7 @@ export const Toolbar = ({ if (comment.length === 0) { return; } - const videoElement = document.querySelector("video"); - const currentTime = videoElement?.currentTime || 0; + const currentTime = playerRef?.current?.currentTime || 0; const optimisticComment: CommentType = { id: `temp-${Date.now()}`, authorId: user?.id || "anonymous", diff --git a/apps/web/app/s/[videoId]/_components/tabs/Activity/Comment.tsx b/apps/web/app/s/[videoId]/_components/tabs/Activity/Comment.tsx index 1258bba108..d88e547016 100644 --- a/apps/web/app/s/[videoId]/_components/tabs/Activity/Comment.tsx +++ b/apps/web/app/s/[videoId]/_components/tabs/Activity/Comment.tsx @@ -94,7 +94,7 @@ const Comment: React.FC<{ transition={{ duration: 0.75, ease: "easeInOut", delay: 0.15 }} className={"flex-1 p-3 rounded-xl border border-gray-3 bg-gray-2"} > -
+

{comment.authorName || "Anonymous"}

diff --git a/apps/web/app/s/[videoId]/_components/tabs/Activity/Comments.tsx b/apps/web/app/s/[videoId]/_components/tabs/Activity/Comments.tsx index a9fb46c852..c25e43bf6c 100644 --- a/apps/web/app/s/[videoId]/_components/tabs/Activity/Comments.tsx +++ b/apps/web/app/s/[videoId]/_components/tabs/Activity/Comments.tsx @@ -7,6 +7,8 @@ import { type ComponentProps, forwardRef, type PropsWithChildren, + type RefObject, + startTransition, useCallback, useEffect, useImperativeHandle, @@ -32,6 +34,7 @@ export const Comments = Object.assign( handleCommentSuccess: (comment: CommentType) => void; onSeek?: (time: number) => void; setShowAuthOverlay: (v: boolean) => void; + playerRef: RefObject; } >((props, ref) => { const { @@ -39,6 +42,7 @@ export const Comments = Object.assign( setOptimisticComments, setComments, handleCommentSuccess, + playerRef, onSeek, } = props; const commentParams = useSearchParams().get("comment"); @@ -74,8 +78,7 @@ export const Comments = Object.assign( const handleNewComment = async (content: string) => { // Get current video time from the video element - const videoElement = document.querySelector("video"); - const currentTime = videoElement?.currentTime || 0; + const currentTime = playerRef?.current?.currentTime || 0; const optimisticComment: CommentType = { id: `temp-${Date.now()}`, @@ -91,7 +94,9 @@ export const Comments = Object.assign( sending: true, }; - setOptimisticComments(optimisticComment); + startTransition(() => { + setOptimisticComments(optimisticComment); + }); try { const data = await newComment({ @@ -109,6 +114,7 @@ export const Comments = Object.assign( const handleReply = async (content: string) => { if (!replyingTo) return; + const currentTime = playerRef?.current?.currentTime || 0; const parentComment = optimisticComments.find((c) => c.id === replyingTo); const actualParentId = parentComment?.parentCommentId @@ -124,12 +130,14 @@ export const Comments = Object.assign( videoId: props.videoId, parentCommentId: actualParentId, type: "text", - timestamp: null, + timestamp: currentTime, updatedAt: new Date(), sending: true, }; - setOptimisticComments(optimisticReply); + startTransition(() => { + setOptimisticComments(optimisticReply); + }); try { const data = await newComment({ @@ -137,6 +145,7 @@ export const Comments = Object.assign( videoId: props.videoId, parentCommentId: actualParentId, type: "text", + timestamp: currentTime, }); handleCommentSuccess(data); diff --git a/apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx b/apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx index 2d9a3ab77f..74d4ade213 100644 --- a/apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx +++ b/apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx @@ -3,7 +3,13 @@ import type { userSelectProps } from "@cap/database/auth/session"; import type { Video } from "@cap/web-domain"; import type React from "react"; -import { forwardRef, type JSX, Suspense, useState } from "react"; +import { + forwardRef, + type JSX, + type RefObject, + Suspense, + useState, +} from "react"; import { CapCardAnalytics } from "@/app/(org)/dashboard/caps/components/CapCard/CapCardAnalytics"; import type { CommentType } from "../../../Share"; import { AuthOverlay } from "../../AuthOverlay"; @@ -21,6 +27,7 @@ interface ActivityProps { optimisticComments: CommentType[]; setOptimisticComments: (newComment: CommentType) => void; isOwnerOrMember: boolean; + playerRef: RefObject; } export const Activity = Object.assign( @@ -30,6 +37,7 @@ export const Activity = Object.assign( user, videoId, isOwnerOrMember, + playerRef, comments, handleCommentSuccess, optimisticComments, @@ -65,6 +73,7 @@ export const Activity = Object.assign( videoId={videoId} setShowAuthOverlay={setShowAuthOverlay} onSeek={props.onSeek} + playerRef={playerRef} /> )} From 0ae32546bd6debd68cad901e5e2dbcf20557239b Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Sat, 27 Sep 2025 14:27:51 +0300 Subject: [PATCH 05/15] cleanup --- apps/web/app/s/[videoId]/page.tsx | 17 +++++++++--- apps/web/lib/Notification.ts | 44 ++++++++++++++++++++++++------- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/apps/web/app/s/[videoId]/page.tsx b/apps/web/app/s/[videoId]/page.tsx index 63a7904dfe..69a5812002 100644 --- a/apps/web/app/s/[videoId]/page.tsx +++ b/apps/web/app/s/[videoId]/page.tsx @@ -317,7 +317,7 @@ export default async function ShareVideoPage(props: PageProps<"/s/[videoId]">) { Effect.succeed({ needsPassword: true } as const), ), Effect.map((data) => ( -
+
{!data.needsPassword && ( @@ -327,7 +327,10 @@ export default async function ShareVideoPage(props: PageProps<"/s/[videoId]">) { Effect.catchTags({ PolicyDenied: () => Effect.succeed( -
+

This video is private @@ -362,13 +365,21 @@ async function AuthorizedContent({ if (user && video && user.id !== video.ownerId) { try { + // Add debugging to see what videoId we're using + console.log( + "Creating view notification for videoId:", + video.id, + "userId:", + user.id, + ); await createNotification({ type: "view", videoId: video.id, authorId: user.id, }); } catch (error) { - console.error("Failed to create view notification:", error); + console.warn("Failed to create view notification:", error); + // Don't throw the error, just log it since this is not critical for the page to function } } diff --git a/apps/web/lib/Notification.ts b/apps/web/lib/Notification.ts index 626d0a1797..62a1415721 100644 --- a/apps/web/lib/Notification.ts +++ b/apps/web/lib/Notification.ts @@ -32,23 +32,49 @@ export async function createNotification( notification: CreateNotificationInput, ) { try { - // First, get the video and owner data - const [videoResult] = await db() + // First, check if the video exists + const [videoExists] = await db() + .select({ id: videos.id, ownerId: videos.ownerId }) + .from(videos) + .where(eq(videos.id, Video.VideoId.make(notification.videoId))) + .limit(1); + + if (!videoExists) { + console.error("Video not found for videoId:", notification.videoId); + throw new Error(`Video not found for videoId: ${notification.videoId}`); + } + + // Then get the owner data + const [ownerResult] = await db() .select({ - videoId: videos.id, - ownerId: users.id, + id: users.id, activeOrganizationId: users.activeOrganizationId, preferences: users.preferences, }) - .from(videos) - .innerJoin(users, eq(users.id, videos.ownerId)) - .where(eq(videos.id, Video.VideoId.make(notification.videoId))) + .from(users) + .where(eq(users.id, videoExists.ownerId)) .limit(1); - if (!videoResult) { - throw new Error("Video or owner not found"); + if (!ownerResult) { + console.warn( + "Owner not found for videoId:", + notification.videoId, + "ownerId:", + videoExists.ownerId, + "- skipping notification creation", + ); + // Don't throw an error, just skip notification creation + // This handles cases where the video exists but the owner was deleted + return; } + const videoResult = { + videoId: videoExists.id, + ownerId: ownerResult.id, + activeOrganizationId: ownerResult.activeOrganizationId, + preferences: ownerResult.preferences, + }; + const { type, ...data } = notification; // Handle replies: notify the parent comment's author From d4091ce88adcc729548c7c812652552b2ef1a884 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Sat, 27 Sep 2025 14:37:42 +0300 Subject: [PATCH 06/15] Update CapVideoPlayer.tsx --- .../s/[videoId]/_components/CapVideoPlayer.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx b/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx index 9116467ff8..6f3ec54ab2 100644 --- a/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx +++ b/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx @@ -526,15 +526,13 @@ export function CapVideoPlayer({ playsInline autoPlay={autoplay} > - {chaptersSrc && } - {captionsSrc && ( - - )} + + )} From 34038cc15f21f7e17c0b7a6b0fa1b17c86bf5c77 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Sat, 27 Sep 2025 14:39:43 +0300 Subject: [PATCH 07/15] Update CapVideoPlayer.tsx --- apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx b/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx index 6f3ec54ab2..f663084d88 100644 --- a/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx +++ b/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx @@ -526,12 +526,12 @@ export function CapVideoPlayer({ playsInline autoPlay={autoplay} > - + )} From 6b04a4d8350ff251b161cc0eb47096faa8a85cbc Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Sat, 27 Sep 2025 14:41:50 +0300 Subject: [PATCH 08/15] Update CapVideoPlayer.tsx --- .../s/[videoId]/_components/CapVideoPlayer.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx b/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx index f663084d88..95a370043f 100644 --- a/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx +++ b/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx @@ -253,6 +253,15 @@ export function CapVideoPlayer({ const [markersReady, setMarkersReady] = useState(false); const [hoveredComment, setHoveredComment] = useState(null); + // Memoize hover handlers to prevent render loops + const handleMouseEnter = useCallback((commentId: string) => { + setHoveredComment(commentId); + }, []); + + const handleMouseLeave = useCallback(() => { + setHoveredComment(null); + }, []); + useEffect(() => { // Only show markers when we have duration, comments, and video element if (duration > 0 && comments.length > 0 && videoRef.current) { @@ -596,7 +605,9 @@ export function CapVideoPlayer({ {markersReady && comments - .filter((comment) => comment.timestamp !== null) + .filter( + (comment) => comment && comment.timestamp !== null && comment.id, + ) .map((comment) => { const position = (Number(comment.timestamp) / duration) * 100; const containerPadding = 20; @@ -612,8 +623,8 @@ export function CapVideoPlayer({ transform: "translateX(-50%)", bottom: "65px", }} - onMouseEnter={() => setHoveredComment(comment.id)} - onMouseLeave={() => setHoveredComment(null)} + onMouseEnter={() => handleMouseEnter(comment.id)} + onMouseLeave={handleMouseLeave} > {/* Comment marker */} - - {hoveredComment === comment.id && ( -
- {/* Arrow pointing down to marker */} -
- -
- {/* User avatar/initial */} - - {/* Comment content */} -
-
- {comment.authorName || "Anonymous"} -
-
- {comment.content} -
-
-
-
- )} -

+ comment={comment} + adjustedPosition={adjustedPosition} + handleMouseEnter={handleMouseEnter} + handleMouseLeave={handleMouseLeave} + onSeek={onSeek} + hoveredComment={hoveredComment} + /> ); })} setMainControlsVisible(arg)} isUploadingOrFailed={isUploading || isUploadFailed} > diff --git a/apps/web/app/s/[videoId]/_components/CommentStamp.tsx b/apps/web/app/s/[videoId]/_components/CommentStamp.tsx new file mode 100644 index 0000000000..8492839f56 --- /dev/null +++ b/apps/web/app/s/[videoId]/_components/CommentStamp.tsx @@ -0,0 +1,85 @@ +import { Avatar } from "@cap/ui"; +import { faComment } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +interface CommentStampsProps { + comment: { + id: string; + timestamp: number | null; + type: "text" | "emoji"; + content: string; + authorName?: string | null; + }; + adjustedPosition: string; + handleMouseEnter: (id: string) => void; + handleMouseLeave: () => void; + onSeek: ((time: number) => void) | undefined; + hoveredComment: string | null; +} + +const CommentStamp: React.FC = ({ + comment, + adjustedPosition, + handleMouseEnter, + handleMouseLeave, + onSeek, + hoveredComment, +}: CommentStampsProps) => { + return ( +
handleMouseEnter(comment.id)} + onMouseLeave={handleMouseLeave} + > + {/* Comment marker */} + + + {hoveredComment === comment.id && ( +
+ {/* Arrow pointing down to marker */} +
+ +
+ {/* User avatar/initial */} + + {/* Comment content */} +
+
+ {comment.authorName || "Anonymous"} +
+
+ {comment.content} +
+
+
+
+ )} +
+ ); +}; + +export default CommentStamp; diff --git a/apps/web/app/s/[videoId]/_components/video/media-player.tsx b/apps/web/app/s/[videoId]/_components/video/media-player.tsx index 2c04228774..ae7c26a9f3 100644 --- a/apps/web/app/s/[videoId]/_components/video/media-player.tsx +++ b/apps/web/app/s/[videoId]/_components/video/media-player.tsx @@ -46,7 +46,7 @@ import { useMediaSelector, } from "media-chrome/react/media-store"; import * as React from "react"; -import { forwardRef } from "react"; +import { forwardRef, useCallback, useEffect } from "react"; import * as ReactDOM from "react-dom"; import { useComposedRefs } from "@/app/lib/compose-refs"; import { cn } from "@/app/lib/utils"; @@ -877,16 +877,29 @@ function MediaPlayerAudio(props: MediaPlayerAudioProps) { interface MediaPlayerControlsProps extends React.ComponentProps<"div"> { asChild?: boolean; isUploadingOrFailed?: boolean; + mainControlsVisible?: (arg: boolean) => void; } function MediaPlayerControls(props: MediaPlayerControlsProps) { - const { asChild, className, isUploadingOrFailed, ...controlsProps } = props; + const { + asChild, + className, + isUploadingOrFailed, + mainControlsVisible, + ...controlsProps + } = props; const context = useMediaPlayerContext("MediaPlayerControls"); const isFullscreen = useMediaSelector( (state) => state.mediaIsFullscreen ?? false, ); const controlsVisible = useStoreSelector((state) => state.controlsVisible); + // Call the callback whenever controlsVisible changes + useEffect(() => { + if (typeof mainControlsVisible === "function") { + mainControlsVisible(controlsVisible); + } + }, [mainControlsVisible, controlsVisible]); const ControlsPrimitive = asChild ? Slot : "div"; @@ -2327,8 +2340,7 @@ function MediaPlayerVolume(props: MediaPlayerVolumeProps) { className={cn( "flex relative items-center select-none touch-none", expandable - ? "w-0 opacity-0 transition-[width,opacity] duration-200 ease-in-out group-focus-within:w-16 group-focus-within:opacity-100 group-hover:w-16 group-hover:opacity-100" - : "w-16", + ? "w-0 opacity-0 transition-[width,opacity] duration-200 ease-in-out group-focus-within:w-16 group-focus-within:opacity-100 group-hover:w-16 group-hover:opacity-100":"w-16", className, )} disabled={isDisabled} From 3cb68c8e4b0d74e1da445fdff642409e0e663521 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Sat, 27 Sep 2025 22:57:03 +0300 Subject: [PATCH 13/15] better seek handling --- apps/web/app/s/[videoId]/Share.tsx | 52 +++++++++++++++++-- .../[videoId]/_components/CapVideoPlayer.tsx | 16 +++--- .../s/[videoId]/_components/ShareVideo.tsx | 5 +- .../app/s/[videoId]/_components/Toolbar.tsx | 45 +++++----------- 4 files changed, 72 insertions(+), 46 deletions(-) diff --git a/apps/web/app/s/[videoId]/Share.tsx b/apps/web/app/s/[videoId]/Share.tsx index 8611d089e5..a336dd484a 100644 --- a/apps/web/app/s/[videoId]/Share.tsx +++ b/apps/web/app/s/[videoId]/Share.tsx @@ -214,9 +214,56 @@ export const Share = ({ const aiLoading = shouldShowLoading(); const handleSeek = useCallback((time: number) => { - if (playerRef.current) { - playerRef.current.currentTime = time; + let video = playerRef.current; + + // Fallback to DOM query if ref is not available + if (!video) { + video = document.querySelector("video") as HTMLVideoElement; + if (!video) { + console.warn("Video player not ready"); + return; + } } + + // Try to seek immediately if video has metadata + if (video.readyState >= 1) { + // HAVE_METADATA + try { + video.currentTime = time; + return; + } catch (error) { + console.error("Failed to seek video:", error); + } + } + + // If video isn't ready, wait for it to be ready + const handleCanPlay = () => { + try { + video.currentTime = time; + } catch (error) { + console.error("Failed to seek video after canplay:", error); + } + video.removeEventListener("canplay", handleCanPlay); + }; + + const handleLoadedMetadata = () => { + try { + video.currentTime = time; + } catch (error) { + console.error("Failed to seek video after loadedmetadata:", error); + } + video.removeEventListener("loadedmetadata", handleLoadedMetadata); + }; + + // Listen for multiple events to ensure we catch when the video is ready + video.addEventListener("canplay", handleCanPlay); + video.addEventListener("loadedmetadata", handleLoadedMetadata); + + // Cleanup after 3 seconds if events don't fire + setTimeout(() => { + video.removeEventListener("canplay", handleCanPlay); + video.removeEventListener("loadedmetadata", handleLoadedMetadata); + }, 3000); }, []); const handleOptimisticComment = useCallback( @@ -252,7 +299,6 @@ export const Share = ({ comments={comments} chapters={aiData?.chapters || []} aiProcessing={aiData?.processing || false} - onSeek={handleSeek} ref={playerRef} />
diff --git a/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx b/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx index a10e9a87e4..7422d5912b 100644 --- a/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx +++ b/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx @@ -533,13 +533,15 @@ export function CapVideoPlayer({ playsInline autoPlay={autoplay} > - - + {chaptersSrc && } + {captionsSrc && ( + + )} )} diff --git a/apps/web/app/s/[videoId]/_components/ShareVideo.tsx b/apps/web/app/s/[videoId]/_components/ShareVideo.tsx index 3e653a0cd1..30e0b78566 100644 --- a/apps/web/app/s/[videoId]/_components/ShareVideo.tsx +++ b/apps/web/app/s/[videoId]/_components/ShareVideo.tsx @@ -41,11 +41,10 @@ export const ShareVideo = forwardRef< comments: MaybePromise; chapters?: { title: string; start: number }[]; aiProcessing?: boolean; - onSeek?: (time: number) => void; } ->(({ data, comments, chapters = [], onSeek }, ref) => { +>(({ data, comments, chapters = [] }, ref) => { const videoRef = useRef(null); - useImperativeHandle(ref, () => videoRef.current as HTMLVideoElement); + useImperativeHandle(ref, () => videoRef.current as HTMLVideoElement, []); const [upgradeModalOpen, setUpgradeModalOpen] = useState(false); const [transcriptData, setTranscriptData] = useState([]); diff --git a/apps/web/app/s/[videoId]/_components/Toolbar.tsx b/apps/web/app/s/[videoId]/_components/Toolbar.tsx index af0105938b..aa17c7d84e 100644 --- a/apps/web/app/s/[videoId]/_components/Toolbar.tsx +++ b/apps/web/app/s/[videoId]/_components/Toolbar.tsx @@ -2,7 +2,13 @@ import type { userSelectProps } from "@cap/database/auth/session"; import type { videos } from "@cap/database/schema"; import { Button } from "@cap/ui"; import { AnimatePresence, motion } from "motion/react"; -import { type RefObject, startTransition, useEffect, useState } from "react"; +import { + type RefObject, + startTransition, + useCallback, + useEffect, + useState, +} from "react"; import { newComment } from "@/actions/videos/new-comment"; import type { CommentType } from "../Share"; import { AuthOverlay } from "./AuthOverlay"; @@ -28,37 +34,10 @@ export const Toolbar = ({ const [commentBoxOpen, setCommentBoxOpen] = useState(false); const [comment, setComment] = useState(""); const [showAuthOverlay, setShowAuthOverlay] = useState(false); - const [videoElement, setVideoElement] = useState( - null, - ); - - useEffect(() => { - const checkForVideoElement = () => { - const element = document.getElementById( - "video-player", - ) as HTMLVideoElement | null; - if (element) { - setVideoElement(element); - } else { - setTimeout(checkForVideoElement, 100); // Check again after 100ms - } - }; - - checkForVideoElement(); - }, []); - - const getTimestamp = (): number => { - if (videoElement) { - return videoElement.currentTime; - } - console.warn("Video element not available, using default timestamp"); - return 0; - }; + const videoElement = playerRef.current; const handleEmojiClick = async (emoji: string) => { - const currentTime = playerRef?.current?.currentTime || 0; - console.log("playerRef", playerRef); - console.log("currentTime", currentTime); + const currentTime = videoElement?.currentTime || 0; const optimisticComment: CommentType = { id: `temp-${Date.now()}`, authorId: user?.id || "anonymous", @@ -98,7 +77,7 @@ export const Toolbar = ({ if (comment.length === 0) { return; } - const currentTime = playerRef?.current?.currentTime || 0; + const currentTime = videoElement?.currentTime || 0; const optimisticComment: CommentType = { id: `temp-${Date.now()}`, authorId: user?.id || "anonymous", @@ -240,8 +219,8 @@ export const Toolbar = ({ handleCommentSubmit(); }} > - {videoElement && getTimestamp() > 0 - ? `Comment at ${getTimestamp().toFixed(2)}` + {videoElement && videoElement.currentTime > 0 + ? `Comment at ${videoElement.currentTime.toFixed(2)}` : "Comment"} Date: Sat, 27 Sep 2025 23:02:25 +0300 Subject: [PATCH 14/15] cleanup --- apps/web/app/s/[videoId]/Share.tsx | 2 -- .../app/s/[videoId]/_components/Sidebar.tsx | 5 +--- .../app/s/[videoId]/_components/Toolbar.tsx | 30 ++++++++++--------- .../_components/tabs/Activity/Comments.tsx | 9 +++--- .../_components/tabs/Activity/index.tsx | 3 -- 5 files changed, 21 insertions(+), 28 deletions(-) diff --git a/apps/web/app/s/[videoId]/Share.tsx b/apps/web/app/s/[videoId]/Share.tsx index a336dd484a..c690044c13 100644 --- a/apps/web/app/s/[videoId]/Share.tsx +++ b/apps/web/app/s/[videoId]/Share.tsx @@ -307,7 +307,6 @@ export const Share = ({ @@ -345,7 +344,6 @@ export const Share = ({ onCommentSuccess={handleCommentSuccess} data={data} user={user} - playerRef={playerRef} />
diff --git a/apps/web/app/s/[videoId]/_components/Sidebar.tsx b/apps/web/app/s/[videoId]/_components/Sidebar.tsx index 5e53349819..bb806ff7bf 100644 --- a/apps/web/app/s/[videoId]/_components/Sidebar.tsx +++ b/apps/web/app/s/[videoId]/_components/Sidebar.tsx @@ -3,7 +3,7 @@ import type { comments as commentsSchema, videos } from "@cap/database/schema"; import { classNames } from "@cap/utils"; import type { Video } from "@cap/web-domain"; import { AnimatePresence, motion } from "framer-motion"; -import { forwardRef, type RefObject, Suspense, useState } from "react"; +import { forwardRef, Suspense, useState } from "react"; import { Activity } from "./tabs/Activity"; import { Settings } from "./tabs/Settings"; import { Summary } from "./tabs/Summary"; @@ -31,7 +31,6 @@ interface SidebarProps { views: MaybePromise; onSeek?: (time: number) => void; videoId: Video.VideoId; - playerRef: RefObject; aiData?: { title?: string | null; summary?: string | null; @@ -76,7 +75,6 @@ export const Sidebar = forwardRef<{ scrollToBottom: () => void }, SidebarProps>( handleCommentSuccess, setOptimisticComments, views, - playerRef, onSeek, videoId, aiData, @@ -135,7 +133,6 @@ export const Sidebar = forwardRef<{ scrollToBottom: () => void }, SidebarProps>( isOwnerOrMember={isOwnerOrMember} onSeek={onSeek} videoId={videoId} - playerRef={playerRef} /> ); diff --git a/apps/web/app/s/[videoId]/_components/Toolbar.tsx b/apps/web/app/s/[videoId]/_components/Toolbar.tsx index aa17c7d84e..66de53b6d9 100644 --- a/apps/web/app/s/[videoId]/_components/Toolbar.tsx +++ b/apps/web/app/s/[videoId]/_components/Toolbar.tsx @@ -2,13 +2,7 @@ import type { userSelectProps } from "@cap/database/auth/session"; import type { videos } from "@cap/database/schema"; import { Button } from "@cap/ui"; import { AnimatePresence, motion } from "motion/react"; -import { - type RefObject, - startTransition, - useCallback, - useEffect, - useState, -} from "react"; +import { startTransition, useCallback, useEffect, useState } from "react"; import { newComment } from "@/actions/videos/new-comment"; import type { CommentType } from "../Share"; import { AuthOverlay } from "./AuthOverlay"; @@ -21,7 +15,6 @@ interface ToolbarProps { user: typeof userSelectProps | null; onOptimisticComment?: (comment: CommentType) => void; onCommentSuccess?: (comment: CommentType) => void; - playerRef: RefObject; } export const Toolbar = ({ @@ -29,14 +22,13 @@ export const Toolbar = ({ user, onOptimisticComment, onCommentSuccess, - playerRef, }: ToolbarProps) => { const [commentBoxOpen, setCommentBoxOpen] = useState(false); const [comment, setComment] = useState(""); const [showAuthOverlay, setShowAuthOverlay] = useState(false); - const videoElement = playerRef.current; const handleEmojiClick = async (emoji: string) => { + const videoElement = document.querySelector("video") as HTMLVideoElement; const currentTime = videoElement?.currentTime || 0; const optimisticComment: CommentType = { id: `temp-${Date.now()}`, @@ -77,6 +69,7 @@ export const Toolbar = ({ if (comment.length === 0) { return; } + const videoElement = document.querySelector("video") as HTMLVideoElement; const currentTime = videoElement?.currentTime || 0; const optimisticComment: CommentType = { id: `temp-${Date.now()}`, @@ -147,6 +140,9 @@ export const Toolbar = ({ setShowAuthOverlay(true); return; } + const videoElement = document.querySelector( + "video", + ) as HTMLVideoElement; if (videoElement) { videoElement.pause(); } @@ -158,13 +154,14 @@ export const Toolbar = ({ return () => { window.removeEventListener("keydown", handleKeyPress); }; - }, [commentBoxOpen, user, videoElement]); + }, [commentBoxOpen, user]); const handleCommentClick = () => { if (!user) { setShowAuthOverlay(true); return; } + const videoElement = document.querySelector("video") as HTMLVideoElement; if (videoElement) { videoElement.pause(); } @@ -219,9 +216,14 @@ export const Toolbar = ({ handleCommentSubmit(); }} > - {videoElement && videoElement.currentTime > 0 - ? `Comment at ${videoElement.currentTime.toFixed(2)}` - : "Comment"} + {(() => { + const videoElement = document.querySelector( + "video", + ) as HTMLVideoElement; + return videoElement && videoElement.currentTime > 0 + ? `Comment at ${videoElement.currentTime.toFixed(2)}` + : "Comment"; + })()} void; onSeek?: (time: number) => void; setShowAuthOverlay: (v: boolean) => void; - playerRef: RefObject; } >((props, ref) => { const { @@ -42,7 +40,6 @@ export const Comments = Object.assign( setOptimisticComments, setComments, handleCommentSuccess, - playerRef, onSeek, } = props; const commentParams = useSearchParams().get("comment"); @@ -78,7 +75,8 @@ export const Comments = Object.assign( const handleNewComment = async (content: string) => { // Get current video time from the video element - const currentTime = playerRef?.current?.currentTime || 0; + const videoElement = document.querySelector("video") as HTMLVideoElement; + const currentTime = videoElement?.currentTime || 0; const optimisticComment: CommentType = { id: `temp-${Date.now()}`, @@ -114,7 +112,8 @@ export const Comments = Object.assign( const handleReply = async (content: string) => { if (!replyingTo) return; - const currentTime = playerRef?.current?.currentTime || 0; + const videoElement = document.querySelector("video") as HTMLVideoElement; + const currentTime = videoElement?.currentTime || 0; const parentComment = optimisticComments.find((c) => c.id === replyingTo); const actualParentId = parentComment?.parentCommentId diff --git a/apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx b/apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx index 74d4ade213..ef7c6c3d57 100644 --- a/apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx +++ b/apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx @@ -27,7 +27,6 @@ interface ActivityProps { optimisticComments: CommentType[]; setOptimisticComments: (newComment: CommentType) => void; isOwnerOrMember: boolean; - playerRef: RefObject; } export const Activity = Object.assign( @@ -37,7 +36,6 @@ export const Activity = Object.assign( user, videoId, isOwnerOrMember, - playerRef, comments, handleCommentSuccess, optimisticComments, @@ -73,7 +71,6 @@ export const Activity = Object.assign( videoId={videoId} setShowAuthOverlay={setShowAuthOverlay} onSeek={props.onSeek} - playerRef={playerRef} /> )} From abfaa9577add1696d6a96c8d8ad72f7c58975394 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Sun, 28 Sep 2025 00:56:52 +0300 Subject: [PATCH 15/15] formatter and cleanup --- apps/web/app/s/[videoId]/Share.tsx | 1 - apps/web/app/s/[videoId]/_components/Toolbar.tsx | 11 ++--------- .../s/[videoId]/_components/video/media-player.tsx | 3 ++- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/apps/web/app/s/[videoId]/Share.tsx b/apps/web/app/s/[videoId]/Share.tsx index c690044c13..1b675b8783 100644 --- a/apps/web/app/s/[videoId]/Share.tsx +++ b/apps/web/app/s/[videoId]/Share.tsx @@ -320,7 +320,6 @@ export const Share = ({ createdAt: effectiveDate, transcriptionStatus, }} - playerRef={playerRef} user={user} commentsData={commentsData} setCommentsData={setCommentsData} diff --git a/apps/web/app/s/[videoId]/_components/Toolbar.tsx b/apps/web/app/s/[videoId]/_components/Toolbar.tsx index 66de53b6d9..db222c4bd5 100644 --- a/apps/web/app/s/[videoId]/_components/Toolbar.tsx +++ b/apps/web/app/s/[videoId]/_components/Toolbar.tsx @@ -2,7 +2,7 @@ import type { userSelectProps } from "@cap/database/auth/session"; import type { videos } from "@cap/database/schema"; import { Button } from "@cap/ui"; import { AnimatePresence, motion } from "motion/react"; -import { startTransition, useCallback, useEffect, useState } from "react"; +import { startTransition, useEffect, useState } from "react"; import { newComment } from "@/actions/videos/new-comment"; import type { CommentType } from "../Share"; import { AuthOverlay } from "./AuthOverlay"; @@ -216,14 +216,7 @@ export const Toolbar = ({ handleCommentSubmit(); }} > - {(() => { - const videoElement = document.querySelector( - "video", - ) as HTMLVideoElement; - return videoElement && videoElement.currentTime > 0 - ? `Comment at ${videoElement.currentTime.toFixed(2)}` - : "Comment"; - })()} + Comment