Skip to content

Conversation

@ameer2468
Copy link
Contributor

@ameer2468 ameer2468 commented Sep 27, 2025

Summary by CodeRabbit

  • New Features

    • Timestamped comments: new comments/replies capture and persist current video time and can be used to seek.
    • In-player comment markers with hover previews; click to jump to timestamps.
    • Exposed video controls visibility for adaptive UI.
    • Navbar: verified custom domains open in a new tab with a link icon; otherwise directs to settings.
  • Bug Fixes

    • More reliable seeking while video loads (robust readiness/listener handling).
    • Safer notification handling when video or owner data is missing.
    • Reduced logging severity for non-critical view errors; stabilized renders.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 27, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds timestamp support to comment creation and optimistic updates; introduces in-player comment markers with seek handling and hover tooltips; propagates onSeek/mainControlsVisible through player components; gates navbar custom-domain link by verification; validates notification video/owner fetches; minor page keying and logging tweaks.

Changes

Cohort / File(s) Summary
Comment timestamp API & optimistic updates
apps/web/actions/videos/new-comment.ts, apps/web/app/s/[videoId]/_components/tabs/Activity/Comments.tsx, apps/web/app/s/[videoId]/_components/tabs/Activity/Comment.tsx, apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx, apps/web/app/s/[videoId]/_components/Toolbar.tsx
Adds required timestamp to newComment payload; reads video.currentTime for optimistic comments and replies; includes timestamp in newComment calls; updates timestamp display/seek handling and forwards onSeek.
Player, seeking, markers, and stamps
apps/web/app/s/[videoId]/Share.tsx, apps/web/app/s/[videoId]/_components/ShareVideo.tsx, apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx, apps/web/app/s/[videoId]/_components/CommentStamp.tsx, apps/web/app/s/[videoId]/_components/video/media-player.tsx
Adds robust handleSeek with readiness listeners and fallback DOM query; CapVideoPlayer accepts comments and onSeek, renders timestamped markers and hover state; new CommentStamp component; exposes mainControlsVisible callback from MediaPlayerControls.
Navbar domain verification gating
apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx
Derives isDomainSetupVerified (customDomain && domainVerified) and conditionally sets href/rel/target/icon/text based on verification.
Notification fetch validation
apps/web/lib/Notification.ts
Splits video/owner retrieval into separate queries; throws if video missing, warns/early-returns if owner missing; constructs consolidated videoResult for downstream logic.
Page identity & logging
apps/web/app/s/[videoId]/page.tsx
Adds key={videoId} to wrapper/fallback divs; downgrades a create-view-notification error to console.warn with explanatory comment.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant T as Toolbar/Comments UI
  participant A as Action newComment
  participant DB as DB
  participant N as Notification

  U->>T: Submit comment (text/emoji)
  T->>T: Read video.currentTime → timestamp
  T->>A: newComment({ content, videoId, type, parentCommentId, timestamp })
  A->>DB: Create comment with timestamp
  alt video & owner present
    A->>N: Create notification
  else missing owner
    A->>N: Skip notification (warn)
  end
  A-->>T: Result
  T-->>U: Optimistic update reconciled
Loading
sequenceDiagram
  autonumber
  participant U as User
  participant CS as CommentStamp
  participant P as CapVideoPlayer
  participant S as Share/ShareVideo
  participant V as <video> element

  U->>CS: Click marker
  CS->>P: onSeek(timestamp)
  P->>S: onSeek(timestamp)
  S->>V: Seek when ready (immediate or via canplay/loadedmetadata)
  V-->>S: Seek applied
Loading
sequenceDiagram
  autonumber
  participant MPC as MediaPlayerControls
  participant P as CapVideoPlayer

  MPC->>MPC: controlsVisible changes
  MPC-->>P: mainControlsVisible(controlsVisible)
  Note right of P: Adjust UI (e.g., cue positioning)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • Brendonovich

Poem

A hop, a mark, a timestamped cheer,
I tap the seek—the scene draws near.
Little dots upon the stream,
Hover, click — the moment's seen.
A rabbit ships a tiny beam. 🐇📼

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly and concisely summarizes the primary change—adding or improving timestamp support for both comments and reactions—and uses a standard “improvement:” prefix, which aligns with the overall modifications in the pull request.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f24ef4b and abfaa95.

📒 Files selected for processing (3)
  • apps/web/app/s/[videoId]/Share.tsx (1 hunks)
  • apps/web/app/s/[videoId]/_components/Toolbar.tsx (9 hunks)
  • apps/web/app/s/[videoId]/_components/video/media-player.tsx (2 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ameer2468 ameer2468 merged commit d653638 into main Sep 27, 2025
14 of 15 checks passed
@ameer2468 ameer2468 deleted the comment-stamps branch September 27, 2025 21:59
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx (1)

95-205: Don't hide pending custom domains

When an org has entered a custom domain but verification hasn’t finished (customDomain truthy, domainVerified false), the new logic flips isDomainSetupVerified to false and we now render “No custom domain set.” That’s inaccurate and removes the only on-screen confirmation of the domain they just configured, likely confusing owners who are still waiting on DNS propagation. Please keep showing the entered domain (optionally mark it as pending) while still routing them back to Settings until verification passes.

Suggested adjustment:

- const isDomainSetupVerified =
-   activeOrg?.organization.customDomain &&
-   activeOrg?.organization.domainVerified;
+ const customDomain = activeOrg?.organization.customDomain;
+ const isDomainSetupVerified = Boolean(
+   customDomain && activeOrg?.organization.domainVerified,
+ );
- href={
-   isDomainSetupVerified
-     ? `https://${activeOrg.organization.customDomain}`
-     : "/dashboard/settings/organization"
- }
+ href={
+   isDomainSetupVerified
+     ? `https://${customDomain}`
+     : "/dashboard/settings/organization"
+ }
- <p className="w-full text-[11px] flex-1 duration-200 truncate leading-0 text-gray-11">
-   {isDomainSetupVerified
-     ? activeOrg?.organization.customDomain
-     : "No custom domain set"}
- </p>
+ <p className="w-full text-[11px] flex-1 duration-200 truncate leading-0 text-gray-11">
+   {customDomain
+     ? isDomainSetupVerified
+       ? customDomain
+       : `${customDomain} (verification pending)`
+     : "No custom domain set"}
+ </p>

This preserves the verification check without regressing the UX for domains that are still pending.

apps/web/lib/Notification.ts (1)

127-134: Guard orgId for reply notifications and skip self-replies

In the reply path you insert with orgId: recipientUser.activeOrganizationId without verifying it exists; unlike the non-reply path, this can insert a null orgId. Also skip notifying when replying to your own comment.

Apply this diff:

 			const notificationId = nanoId();
 
-			await db().insert(notifications).values({
+			// Skip if recipient has no active org
+			if (!recipientUser.activeOrganizationId) {
+				console.warn(
+					`Reply recipient ${recipientId} has no active organization, skipping notification`,
+				);
+				return;
+			}
+			// Skip self-replies
+			if (recipientId === notification.authorId) return;
+
+			await db().insert(notifications).values({
 				id: notificationId,
 				orgId: recipientUser.activeOrganizationId,
 				recipientId,
 				type,
 				data,
 			});
apps/web/app/s/[videoId]/Share.tsx (3)

79-140: Switch to EffectRuntime’s useEffectQuery (repo rule).

Client-side server state must use useEffectQuery from @/lib/EffectRuntime, not useQuery. Replace the import and call to align with apps/web guidelines.

Apply:

-import { useQuery } from "@tanstack/react-query";
+import { useEffectQuery } from "@/lib/EffectRuntime";
@@
-  return useQuery({
+  return useEffectQuery({
     queryKey: ["videoStatus", videoId],
     queryFn: async (): Promise<VideoStatusResult> => {
       const res = await getVideoStatus(videoId);
       if ("success" in res && res.success === false)
         throw new Error("Failed to fetch video status");
       return res as VideoStatusResult;
     },

1-427: Remove inline comments per repo guideline.

Multiple inline comments exist in this client module. Repo rule forbids inline/block/doc comments in TS/TSX. Remove them (your handleSeek diff above already addresses the main block).


323-336: Add playerRef to SidebarProps. Sidebar is already wrapped with forwardRef; update the props interface in
apps/web/app/s/[videoId]/_components/Sidebar.tsx:

 interface SidebarProps {
   // existing props…
+  playerRef?: React.RefObject<HTMLVideoElement | null>;
 }
apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx (2)

376-381: Duplicate loadedmetadata listener attached.

loadedmetadata is added twice; handler will fire twice. Remove the duplicate to avoid double work and inconsistent state.

Apply:

-    video.addEventListener("loadedmetadata", handleLoadedMetadataWithTracks);
-    video.addEventListener("load", handleLoad);
+    video.addEventListener("load", handleLoad);
@@
-    video.addEventListener("loadedmetadata", handleLoadedMetadataWithTracks);

1-1: Rename CapVideoPlayer.tsx to kebab-case
Rename apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsxapps/web/app/s/[videoId]/_components/cap-video-player.tsx to comply with the repo’s kebab-case filename convention.

🧹 Nitpick comments (16)
apps/web/actions/videos/new-comment.ts (2)

28-29: Validate and normalize timestamp; avoid storing invalid values and replies with timestamps

Guard against NaN/negative inputs and ensure replies don’t carry timestamps.

Apply this diff:

-	const timestamp = data.timestamp;
+	const timestamp =
+		Number.isFinite(data.timestamp) && data.timestamp >= 0
+			? Math.floor(data.timestamp)
+			: null;
-		timestamp: timestamp ?? null,
+		timestamp: parentCommentId ? null : timestamp,

Also applies to: 46-46


73-73: Prefer targeted client cache updates over broad route revalidation

Revalidating /s/${videoId} is heavy. When invoked from the client, favor TanStack Query v5’s setQueryData/setQueriesData for the comments list and avoid broad invalidations.

apps/web/app/s/[videoId]/_components/tabs/Activity/Comment.tsx (1)

97-101: Make timestamp rendering robust; avoid deprecated substr and guard for finite values

Ensure the timestamp is finite/non-negative, preserve 0s, and avoid substr.

Apply this diff:

-							{comment.timestamp !== null && (
+							{Number.isFinite(Number(comment.timestamp)) &&
+								Number(comment.timestamp) >= 0 && (
 								<button
 									type="button"
-									onClick={() => {
-										onSeek?.(Number(comment.timestamp));
-									}}
+									onClick={() => onSeek?.(Math.floor(Number(comment.timestamp)))}
 									className="text-xs text-blue-500 cursor-pointer hover:text-blue-700"
 								>
-									{new Date(comment.timestamp * 1000)
-										.toISOString()
-										.substr(11, 8)}
+									{new Date(Math.floor(Number(comment.timestamp)) * 1000)
+										.toISOString()
+										.slice(11, 19)}
 								</button>
 							)}

Also applies to: 102-120, 122-122

apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx (1)

6-12: onSeek propagation LGTM; drop unused imports

Forwarding onSeek is correct. If RefObject isn’t used here, remove it to keep imports lean.

Also applies to: 73-74

apps/web/app/s/[videoId]/_components/video/media-player.tsx (1)

897-903: Remove inline comment to comply with repo guidelines.

Inline comment violates “no comments in TS/TSX” rule.

Apply this diff:

-	// Call the callback whenever controlsVisible changes
 	useEffect(() => {
 		if (typeof mainControlsVisible === "function") {
 			mainControlsVisible(controlsVisible);
 		}
 	}, [mainControlsVisible, controlsVisible]);

As per coding guidelines.

apps/web/app/s/[videoId]/_components/tabs/Activity/Comments.tsx (2)

77-80: Avoid querying the DOM repeatedly; centralize currentTime retrieval.

DRY and reduce null checks by using a tiny helper.

Apply this diff:

+		const getVideoCurrentTime = () => {
+			const el = document.querySelector("video") as HTMLVideoElement | null;
+			return el?.currentTime ?? 0;
+		};
...
-			// Get current video time from the video element
-			const videoElement = document.querySelector("video") as HTMLVideoElement;
-			const currentTime = videoElement?.currentTime || 0;
+			const currentTime = getVideoCurrentTime();
...
-			const videoElement = document.querySelector("video") as HTMLVideoElement;
-			const currentTime = videoElement?.currentTime || 0;
+			const currentTime = getVideoCurrentTime();

Also applies to: 115-117


77-77: Remove inline comment to meet repo standards.

Comments in TSX aren’t allowed in this codebase.

-			// Get current video time from the video element

As per coding guidelines.

apps/web/app/s/[videoId]/_components/CommentStamp.tsx (3)

30-30: Don’t set key inside the component.

Parent supplies keys; local key is redundant and can confuse React.

-			key={comment.id}

41-41: Remove inline JSX comments to match repo policy.

Comments in TSX are disallowed here.

-			{/* Comment marker */}
...
-					{/* Arrow pointing down to marker */}
...
-						{/* User avatar/initial */}
...
-						{/* Comment content */}

As per coding guidelines.

Also applies to: 59-61, 64-69, 70-77


1-86: File naming: prefer kebab-case.

Please rename to comment-stamp.tsx and update imports.

As per coding guidelines.

apps/web/app/s/[videoId]/_components/Toolbar.tsx (1)

31-33: Consolidate currentTime reads.

Use a small helper to avoid repeating querySelector logic and potential null checks.

+	const getVideoCurrentTime = () => {
+		const el = document.querySelector("video") as HTMLVideoElement | null;
+		return el?.currentTime ?? 0;
+	};
...
-		const videoElement = document.querySelector("video") as HTMLVideoElement;
-		const currentTime = videoElement?.currentTime || 0;
+		const currentTime = getVideoCurrentTime();
...
-		const videoElement = document.querySelector("video") as HTMLVideoElement;
-		const currentTime = videoElement?.currentTime || 0;
+		const currentTime = getVideoCurrentTime();
...
-				const videoElement = document.querySelector(
-					"video",
-				) as HTMLVideoElement;
+				const videoElement = document.querySelector("video") as HTMLVideoElement | null;
...
-									{(() => {
-										const videoElement = document.querySelector(
-											"video",
-										) as HTMLVideoElement;
-										return videoElement && videoElement.currentTime > 0
-											? `Comment at ${videoElement.currentTime.toFixed(2)}`
-											: "Comment";
-									})()}
+									{(() => {
+										const t = getVideoCurrentTime();
+										return t > 0 ? `Comment at ${t.toFixed(2)}` : "Comment";
+									})()}

Also applies to: 72-74, 143-146, 219-226

apps/web/app/s/[videoId]/_components/ShareVideo.tsx (2)

45-48: Props mismatch: type declares aiProcessing but implementation drops it.

Keep the public surface consistent; safely accept and ignore the prop.

->(({ data, comments, chapters = [] }, ref) => {
+>(({ data, comments, chapters = [], aiProcessing: _aiProcessing }, ref) => {

61-69: Remove inline comments per repo policy.

Replace narrative comments with self-explanatory names; comments are not allowed.

-	// Handle comments data
 	useEffect(() => {
...
-	// Handle seek functionality
 	const handleSeek = (time: number) => {

As per coding guidelines.

Also applies to: 71-77

apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx (2)

234-249: Avoid inline comments; keep effect scoped to ref changes.

Remove comments per repo rule. Optionally include videoRef in deps if ref identity can change.

Apply:

-// Track video duration for comment markers
 useEffect(() => {
   const video = videoRef.current;
   if (!video) return;
-
-  const handleLoadedMetadata = () => {
+  const handleLoadedMetadata = () => {
     setDuration(video.duration);
   };
-
   video.addEventListener("loadedmetadata", handleLoadedMetadata);
-
   return () => {
     video.removeEventListener("loadedmetadata", handleLoadedMetadata);
   };
-}, [urlResolved]);
+}, [urlResolved]);

606-629: Clamp marker positions and guard against invalid timestamps.

Prevents NaN/overflow when incoming timestamp is out of range; keeps markers within bounds.

Apply:

-          .map((comment) => {
-            const position = (Number(comment.timestamp) / duration) * 100;
+          .map((comment) => {
+            const ts = Math.max(
+              0,
+              Math.min(duration - 0.001, Number(comment.timestamp)),
+            );
+            const position = (ts / duration) * 100;
             const containerPadding = 20;
             const availableWidth = `calc(100% - ${containerPadding * 2}px)`;
             const adjustedPosition = `calc(${containerPadding}px + (${position}% * ${availableWidth} / 100%))`;
apps/web/app/s/[videoId]/Share.tsx (1)

1-1: Rename Share.tsx → share.tsx
git mv apps/web/app/s/[videoId]/Share.tsx apps/web/app/s/[videoId]/share.tsx
Update import paths to reference share.tsx.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 273f2db and f24ef4b.

📒 Files selected for processing (13)
  • apps/web/actions/videos/new-comment.ts (3 hunks)
  • apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx (2 hunks)
  • apps/web/app/s/[videoId]/Share.tsx (2 hunks)
  • apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx (8 hunks)
  • apps/web/app/s/[videoId]/_components/CommentStamp.tsx (1 hunks)
  • apps/web/app/s/[videoId]/_components/ShareVideo.tsx (2 hunks)
  • apps/web/app/s/[videoId]/_components/Toolbar.tsx (10 hunks)
  • apps/web/app/s/[videoId]/_components/tabs/Activity/Comment.tsx (1 hunks)
  • apps/web/app/s/[videoId]/_components/tabs/Activity/Comments.tsx (7 hunks)
  • apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx (2 hunks)
  • apps/web/app/s/[videoId]/_components/video/media-player.tsx (3 hunks)
  • apps/web/app/s/[videoId]/page.tsx (3 hunks)
  • apps/web/lib/Notification.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
apps/web/actions/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

All Groq/OpenAI calls must be implemented in Next.js Server Actions under apps/web/actions; do not place AI calls elsewhere

Files:

  • apps/web/actions/videos/new-comment.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/**/*.{ts,tsx}: Use TanStack Query v5 for all client-side server state and data fetching in the web app
Web mutations should call Server Actions directly and perform targeted cache updates with setQueryData/setQueriesData rather than broad invalidations
Client code should use useEffectQuery/useEffectMutation and useRpcClient from apps/web/lib/EffectRuntime.ts; do not create ManagedRuntime inside components

Files:

  • apps/web/actions/videos/new-comment.ts
  • apps/web/app/s/[videoId]/_components/ShareVideo.tsx
  • apps/web/app/s/[videoId]/page.tsx
  • apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx
  • apps/web/app/s/[videoId]/_components/CommentStamp.tsx
  • apps/web/lib/Notification.ts
  • apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx
  • apps/web/app/s/[videoId]/_components/tabs/Activity/Comment.tsx
  • apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx
  • apps/web/app/s/[videoId]/Share.tsx
  • apps/web/app/s/[videoId]/_components/video/media-player.tsx
  • apps/web/app/s/[videoId]/_components/tabs/Activity/Comments.tsx
  • apps/web/app/s/[videoId]/_components/Toolbar.tsx
**/*.{ts,tsx,js,jsx,rs}

📄 CodeRabbit inference engine (CLAUDE.md)

Do not add inline, block, or docstring comments in any language; code must be self-explanatory

Files:

  • apps/web/actions/videos/new-comment.ts
  • apps/web/app/s/[videoId]/_components/ShareVideo.tsx
  • apps/web/app/s/[videoId]/page.tsx
  • apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx
  • apps/web/app/s/[videoId]/_components/CommentStamp.tsx
  • apps/web/lib/Notification.ts
  • apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx
  • apps/web/app/s/[videoId]/_components/tabs/Activity/Comment.tsx
  • apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx
  • apps/web/app/s/[videoId]/Share.tsx
  • apps/web/app/s/[videoId]/_components/video/media-player.tsx
  • apps/web/app/s/[videoId]/_components/tabs/Activity/Comments.tsx
  • apps/web/app/s/[videoId]/_components/Toolbar.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use strict TypeScript and avoid any; leverage shared types from packages

**/*.{ts,tsx}: Use a 2-space indent for TypeScript code.
Use Biome for formatting and linting TypeScript/JavaScript files by running pnpm format.

Files:

  • apps/web/actions/videos/new-comment.ts
  • apps/web/app/s/[videoId]/_components/ShareVideo.tsx
  • apps/web/app/s/[videoId]/page.tsx
  • apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx
  • apps/web/app/s/[videoId]/_components/CommentStamp.tsx
  • apps/web/lib/Notification.ts
  • apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx
  • apps/web/app/s/[videoId]/_components/tabs/Activity/Comment.tsx
  • apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx
  • apps/web/app/s/[videoId]/Share.tsx
  • apps/web/app/s/[videoId]/_components/video/media-player.tsx
  • apps/web/app/s/[videoId]/_components/tabs/Activity/Comments.tsx
  • apps/web/app/s/[videoId]/_components/Toolbar.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use kebab-case for filenames for TypeScript/JavaScript modules (e.g., user-menu.tsx).
Use PascalCase for React/Solid components.

Files:

  • apps/web/actions/videos/new-comment.ts
  • apps/web/app/s/[videoId]/_components/ShareVideo.tsx
  • apps/web/app/s/[videoId]/page.tsx
  • apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx
  • apps/web/app/s/[videoId]/_components/CommentStamp.tsx
  • apps/web/lib/Notification.ts
  • apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx
  • apps/web/app/s/[videoId]/_components/tabs/Activity/Comment.tsx
  • apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx
  • apps/web/app/s/[videoId]/Share.tsx
  • apps/web/app/s/[videoId]/_components/video/media-player.tsx
  • apps/web/app/s/[videoId]/_components/tabs/Activity/Comments.tsx
  • apps/web/app/s/[videoId]/_components/Toolbar.tsx
apps/web/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

On the client, always use useEffectQuery or useEffectMutation from @/lib/EffectRuntime; never call EffectRuntime.run* directly in components.

Files:

  • apps/web/actions/videos/new-comment.ts
  • apps/web/app/s/[videoId]/_components/ShareVideo.tsx
  • apps/web/app/s/[videoId]/page.tsx
  • apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx
  • apps/web/app/s/[videoId]/_components/CommentStamp.tsx
  • apps/web/lib/Notification.ts
  • apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx
  • apps/web/app/s/[videoId]/_components/tabs/Activity/Comment.tsx
  • apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx
  • apps/web/app/s/[videoId]/Share.tsx
  • apps/web/app/s/[videoId]/_components/video/media-player.tsx
  • apps/web/app/s/[videoId]/_components/tabs/Activity/Comments.tsx
  • apps/web/app/s/[videoId]/_components/Toolbar.tsx
apps/web/app/**/*.{tsx,ts}

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer Server Components for initial data in the Next.js App Router and pass initialData to client components

Files:

  • apps/web/app/s/[videoId]/_components/ShareVideo.tsx
  • apps/web/app/s/[videoId]/page.tsx
  • apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx
  • apps/web/app/s/[videoId]/_components/CommentStamp.tsx
  • apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx
  • apps/web/app/s/[videoId]/_components/tabs/Activity/Comment.tsx
  • apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx
  • apps/web/app/s/[videoId]/Share.tsx
  • apps/web/app/s/[videoId]/_components/video/media-player.tsx
  • apps/web/app/s/[videoId]/_components/tabs/Activity/Comments.tsx
  • apps/web/app/s/[videoId]/_components/Toolbar.tsx
🧬 Code graph analysis (5)
apps/web/app/s/[videoId]/_components/ShareVideo.tsx (3)
apps/web/app/s/[videoId]/_components/utils/transcript-utils.ts (1)
  • TranscriptEntry (3-8)
apps/web/hooks/use-transcript.ts (1)
  • useTranscript (5-27)
packages/database/schema.ts (1)
  • comments (312-332)
apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx (1)
packages/database/schema.ts (1)
  • comments (312-332)
apps/web/lib/Notification.ts (2)
packages/database/index.ts (1)
  • db (29-34)
packages/database/schema.ts (2)
  • videos (240-288)
  • users (48-96)
apps/web/app/s/[videoId]/_components/tabs/Activity/Comment.tsx (1)
apps/web/app/s/[videoId]/_components/tabs/Activity/utils.ts (2)
  • formatTimestamp (1-10)
  • formatTimeAgo (12-32)
apps/web/app/s/[videoId]/_components/tabs/Activity/Comments.tsx (1)
apps/web/actions/videos/new-comment.ts (1)
  • newComment (11-76)
🪛 GitHub Check: Typecheck
apps/web/app/s/[videoId]/Share.tsx

[failure] 323-323:
Type '{ data: { createdAt: Date; transcriptionStatus: "PROCESSING" | "COMPLETE" | "ERROR" | null; metadata: VideoMetadata | null; name: string; id: string & Brand<"VideoId">; ... 25 more ...; ownerIsPro?: boolean | undefined; }; ... 12 more ...; ref: RefObject<...>; }' is not assignable to type 'IntrinsicAttributes & SidebarProps & RefAttributes<{ scrollToBottom: () => void; }>'.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Vercel Agent Review
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (11)
apps/web/app/s/[videoId]/page.tsx (1)

320-325: Verify necessity of key={videoId} on top-level wrappers

Adding keys on these non-list roots will force remounts when videoId changes. If the intent is to reset local client state (e.g., player/optimistic UI) between videos, keep it; otherwise, consider removing to preserve state across param updates.

Also applies to: 330-333

apps/web/app/s/[videoId]/_components/video/media-player.tsx (2)

877-891: New mainControlsVisible callback is wired correctly.

Prop type and destructuring look good; callback exposure from Controls is appropriate.


2343-2344: Volume slider expand/collapse transition—LGTM.

ClassName tweak improves affordance without changing behavior.

apps/web/app/s/[videoId]/_components/tabs/Activity/Comments.tsx (2)

59-60: Scroll behavior dependency update—LGTM.

Including commentParams/replyParams prevents unintended auto-scroll after deep links.


70-71: API propagation—LGTM.

Exposing scrollToBottom via ref and passing onSeek to Comment is correct.

Also applies to: 213-214

apps/web/app/s/[videoId]/_components/Toolbar.tsx (1)

90-101: Optimistic + server action flow—LGTM.

startTransition usage and timestamp plumbing align with the new API.

Also applies to: 219-227

apps/web/app/s/[videoId]/_components/ShareVideo.tsx (1)

178-186: comments prop normalization—LGTM.

Mapping to the player’s simple shape and wiring onSeek is correct.

apps/web/app/s/[videoId]/Share.tsx (2)

271-274: startTransition usage for optimistic UI looks good.


156-165: React and React-DOM are consistently 19.1.1 across packages; use() is supported.

apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx (2)

536-545: Tracks conditional rendering LGTM.


633-633: mainControlsVisible callback integration LGTM.

content: content,
videoId: videoId,
timestamp: null,
timestamp: timestamp || null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: 0-second timestamps are coerced to null

Using timestamp || null drops valid 0 into null. Use nullish coalescing.

Apply this diff:

-		timestamp: timestamp || null,
+		timestamp: timestamp ?? null,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
timestamp: timestamp || null,
timestamp: timestamp ?? null,
🤖 Prompt for AI Agents
In apps/web/actions/videos/new-comment.ts around line 46, the assignment
`timestamp: timestamp || null` incorrectly converts a valid 0 timestamp to null;
replace the logical OR with nullish coalescing so that only undefined or null
become null (i.e., use timestamp ?? null) and ensure the updated expression
preserves falsy numeric 0 values.

Comment on lines 100 to 107
const data = await newComment({
content,
videoId: props.videoId,
parentCommentId: "",
type: "text",
timestamp: currentTime,
});
handleCommentSuccess(data);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

0-second timestamps are being saved as null (server action bug).

new-comment.ts uses timestamp || null, which coerces 0 to null. This breaks exact-start markers.

Apply this diff in apps/web/actions/videos/new-comment.ts:

-  const timestamp = data.timestamp;
...
-    timestamp: timestamp || null,
+  const timestamp = data.timestamp;
+  // Preserve 0s; only null/undefined become null in DB
+    timestamp: timestamp ?? null,

Based on relevant code snippet.

Also applies to: 142-148

Comment on lines +374 to 376
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
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Log level change is good; remove inline comment per repo guidelines

console.warn for a non-critical failure is appropriate. However, inline comments are disallowed in TS/TSX per coding guidelines. Please drop the added comment line.

Apply this diff:

-			// Don't throw the error, just log it since this is not critical for the page to function
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
}
console.warn("Failed to create view notification:", error);
}
🤖 Prompt for AI Agents
In apps/web/app/s/[videoId]/page.tsx around lines 374 to 376, you added a
console.warn followed by an inline comment; remove the inline comment line "//
Don't throw the error, just log it since this is not critical for the page to
function" so the file contains only the console.warn call (or equivalent
logging) and no inline comment, complying with TS/TSX repo guidelines.

Comment on lines +216 to 268
const handleSeek = useCallback((time: number) => {
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);
}, []);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make seeking idempotent; use once listeners, clamp time, and remove inline comments.

Avoid duplicate seeks and dangling listeners; clamp against duration when known; remove comments per repo rule.

Apply:

-const handleSeek = useCallback((time: number) => {
-  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 handleSeek = useCallback((time: number) => {
+  const v =
+    playerRef.current ?? (document.querySelector("video") as HTMLVideoElement | null);
+  if (!v) {
+    console.warn("Video player not ready");
+    return;
+  }
+  const seekOnce = (t: number) => {
+    const dur = Number.isFinite(v.duration) && v.duration > 0 ? v.duration : null;
+    const clamped = dur ? Math.max(0, Math.min(dur - 0.001, t)) : t;
+    try {
+      v.currentTime = clamped;
+    } catch (e) {
+      console.error("Failed to seek video:", e);
+    }
+  };
+  if (v.readyState >= 1) {
+    seekOnce(time);
+    return;
+  }
+  let timeoutId: ReturnType<typeof setTimeout> | null = null;
+  const handleReady = () => {
+    seekOnce(time);
+    v.removeEventListener("canplay", handleReady);
+    v.removeEventListener("loadedmetadata", handleReady);
+    if (timeoutId) clearTimeout(timeoutId);
+  };
+  v.addEventListener("canplay", handleReady, { once: true });
+  v.addEventListener("loadedmetadata", handleReady, { once: true });
+  timeoutId = setTimeout(() => {
+    v.removeEventListener("canplay", handleReady);
+    v.removeEventListener("loadedmetadata", handleReady);
+  }, 3000);
+}, []);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleSeek = useCallback((time: number) => {
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 handleSeek = useCallback((time: number) => {
const v =
playerRef.current ?? (document.querySelector("video") as HTMLVideoElement | null);
if (!v) {
console.warn("Video player not ready");
return;
}
const seekOnce = (t: number) => {
const dur = Number.isFinite(v.duration) && v.duration > 0 ? v.duration : null;
const clamped = dur ? Math.max(0, Math.min(dur - 0.001, t)) : t;
try {
v.currentTime = clamped;
} catch (e) {
console.error("Failed to seek video:", e);
}
};
if (v.readyState >= 1) {
seekOnce(time);
return;
}
let timeoutId: ReturnType<typeof setTimeout> | null = null;
const handleReady = () => {
seekOnce(time);
v.removeEventListener("canplay", handleReady);
v.removeEventListener("loadedmetadata", handleReady);
if (timeoutId) clearTimeout(timeoutId);
};
v.addEventListener("canplay", handleReady, { once: true });
v.addEventListener("loadedmetadata", handleReady, { once: true });
timeoutId = setTimeout(() => {
v.removeEventListener("canplay", handleReady);
v.removeEventListener("loadedmetadata", handleReady);
}, 3000);
}, []);
🤖 Prompt for AI Agents
In apps/web/app/s/[videoId]/Share.tsx around lines 216-268, the seek handler can
perform duplicate seeks and leave dangling listeners; update handleSeek to clamp
requested time to [0, video.duration] when duration is finite, only set
video.currentTime if the difference from currentTime is greater than a small
epsilon (to make seeking idempotent), attach event listeners with the { once:
true } option (or remove them immediately after firing) instead of manual
removal, and keep the handler cleanup timeout but ensure it clears listeners
only if still present; also remove inline comments per repo style.

Comment on lines +35 to +46
// 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}`);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Good early validations; remove newly added inline comments to meet repo policy

The two-step video/owner checks are solid. However, inline comments were added in these hunks; comments are disallowed in TS/TSX here. Please remove them.

Example diff (remove only comment lines):

-		// First, check if the video exists
...
-		// Then get the owner data
...
-			// Don't throw an error, just skip notification creation
-			// This handles cases where the video exists but the owner was deleted
...
-		// 

Also applies to: 47-56, 58-69, 71-77

🤖 Prompt for AI Agents
In apps/web/lib/Notification.ts around lines 35-46 (and also apply the same
change to ranges 47-56, 58-69, 71-77), remove the newly added inline comment
lines so the TypeScript/TSX file contains no inline comments in these hunks;
keep the existing validation code and error handling intact but delete only the
comment lines to comply with repo policy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants