Skip to content

feat(mobile-expo): VideoHeroRenderer section component#321

Open
up-tandem wants to merge 4 commits intomainfrom
feat/306-video-hero-renderer
Open

feat(mobile-expo): VideoHeroRenderer section component#321
up-tandem wants to merge 4 commits intomainfrom
feat/306-video-hero-renderer

Conversation

@up-tandem
Copy link
Contributor

@up-tandem up-tandem commented Mar 10, 2026

Summary

  • Replaces the VideoHero stub renderer with a real implementation that displays a full-width hero banner
  • Uses ImageBackground for video thumbnail with dark overlay for text legibility
  • Renders heading, subheading, and CTA button (Pressable + Linking.openURL)
  • Gracefully handles missing optional fields (no CTA if ctaLabel/ctaLink absent, fallback background when no thumbnail)
  • Accessible labels on all interactive elements

Contracts Changed

No

Regeneration Required

No

Validation

  • pnpm --filter @forge/expo lint — passes with 0 warnings
  • pnpm --filter @forge/expo test -- --testPathPattern=VideoHeroRenderer — 4/4 tests pass
  • pnpm --filter @forge/expo test -- --testPathPattern=SectionDispatcher — 11/11 tests pass (no regressions)

Resolves #306

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Video Hero now supports streaming playback (with thumbnail/fallback), heading/subheading overlay, and CTA behavior; content can include streaming URLs.
  • Tests

    • Added unit tests covering rendering with various combinations of required and optional fields.
  • Chores

    • Enabled Expo video support in app config and test setup (mocked video APIs for Jest).

Replace the VideoHero stub with a real renderer that displays a hero
banner with video thumbnail background, heading, subheading, and CTA
button. Handles missing optional fields gracefully.

Resolves #306

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 10, 2026

Walkthrough

Adds a new VideoHeroRenderer (replacing the stub), extends VideoHeroSection with streamingUrl, integrates expo-video (dependency, plugin, Jest mock), updates exports and tests, and adds unit tests for the new renderer.

Changes

Cohort / File(s) Summary
New renderer
mobile/expo/src/components/sections/VideoHeroRenderer.tsx
Implements inline video hero rendering via expo-video VideoView, poster/thumbnail fallback, play/pause overlay, heading/subheading overlay, conditional CTA (opens link), accessibility attributes, and exported VideoHeroRendererProps.
Tests
mobile/expo/src/components/sections/VideoHeroRenderer.test.tsx
Adds unit tests covering rendering across combinations of required/optional fields to ensure the component renders without throwing.
Stub removed
mobile/expo/src/components/sections/VideoHeroStub.tsx
Removes the previous stub renderer and its exported types.
Dispatcher & exports
mobile/expo/src/components/sections/SectionDispatcher.tsx, mobile/expo/src/components/sections/index.ts
Replaces imports/exports to point to ./VideoHeroRenderer instead of the removed stub; also removed two TypeScript suppression comments in SectionDispatcher.
Model & mapper
mobile/expo/src/lib/sectionModels.ts, mobile/expo/src/lib/sectionMapper.ts
Adds optional `streamingUrl: string
GraphQL
packages/graphql/src/watchExperience.ts
Adds ComponentSectionsVideoHero fragment fields including streamingUrl and nested heroVideo image metadata to the GetWatchExperience query.
Expo integration & test setup
mobile/expo/package.json, mobile/expo/app.json, mobile/expo/jest.setup.js
Adds expo-video dependency, registers "expo-video" in expo.plugins, and adds a Jest mock for expo-video (mocked useVideoPlayer and VideoView).
Tests & config updates
mobile/expo/src/components/sections/SectionDispatcher.test.tsx, mobile/expo/eslint.config.js
Adds streamingUrl (null) to test fixture and updates ESLint config for Jest globals.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant Renderer as VideoHeroRenderer
  participant PlayerHook as useVideoPlayer
  participant Video as VideoView
  participant Linking as Linking.openURL

  User->>Renderer: tap Play
  Renderer->>PlayerHook: toggle play/pause
  PlayerHook->>Video: play (HLS) / pause
  Video-->>Renderer: playback state update

  User->>Renderer: tap CTA
  Renderer->>Linking: openURL(ctaLink)
  Linking-->>User: external link opened
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(mobile-expo): VideoHeroRenderer section component' directly and concisely summarizes the primary change: implementing a new VideoHeroRenderer component for the mobile Expo platform.
Linked Issues check ✅ Passed All acceptance criteria from issue #306 are met: inline video playback with expo-video [#306], heading/subheading rendering [#306], video thumbnail poster [#306], play/pause controls [#306], CTA button [#306], full-width layout [#306], graceful handling of missing fields [#306], stub replacement [#306], and accessibility features [#306].
Out of Scope Changes check ✅ Passed All changes directly support the VideoHeroRenderer implementation: new component files, model updates, GraphQL query additions, and test coverage—no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/306-video-hero-renderer

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.

Copy link

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mobile/expo/src/components/sections/VideoHeroRenderer.tsx`:
- Around line 20-24: handleCtaPress currently treats any non-null ctaLink as
valid and calls Linking.openURL while ignoring failures; change handleCtaPress
to first trim and validate ctaLink (ensure it's non-empty after trim and matches
a valid URL scheme), then use Linking.canOpenURL(ctaLink) and only call
Linking.openURL if canOpenURL resolves true, wrapping the open call in a
try/catch to handle rejected promises (log or surface an error) and avoid
creating a visible CTA when ctaLink is empty/whitespace; reference
handleCtaPress, ctaLink, ctaLabel, and Linking.openURL/Linking.canOpenURL when
locating where to implement these checks.
- Around line 27-81: The JSX suppression comments in VideoHeroRenderer.tsx hide
prop type errors; import JSX from React and annotate the component and local JSX
values instead of using `@ts-expect-error`: add import { JSX } from "react",
change the VideoHeroRenderer function signature to return JSX.Element (and type
the local content variable as JSX.Element | null if present), then remove the
inline `@ts-expect-error` comments on View, Text, Pressable, and ImageBackground
so React Native prop types are checked properly (references: VideoHeroRenderer,
content, thumbnailUrl, and the JSX elements
View/Text/Pressable/ImageBackground).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6b93c46a-b0e7-4fc6-8adf-cf148c310a4b

📥 Commits

Reviewing files that changed from the base of the PR and between 2bc0bcd and f1bd2f6.

📒 Files selected for processing (5)
  • mobile/expo/src/components/sections/SectionDispatcher.tsx
  • mobile/expo/src/components/sections/VideoHeroRenderer.test.tsx
  • mobile/expo/src/components/sections/VideoHeroRenderer.tsx
  • mobile/expo/src/components/sections/VideoHeroStub.tsx
  • mobile/expo/src/components/sections/index.ts
💤 Files with no reviewable changes (1)
  • mobile/expo/src/components/sections/VideoHeroStub.tsx

…roRenderer

Trim ctaLabel and ctaLink before rendering to prevent showing a CTA button
with empty or whitespace-only values from CMS data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@up-tandem
Copy link
Contributor Author

Review feedback addressed (e12a03b)

Handled:

  • [Handle blank/whitespace CTA links]: Added trim guards for ctaLabel and ctaLink — empty/whitespace values no longer render a CTA button. Skipped canOpenURL + try/catch as CMS-controlled data makes invalid URLs unlikely.

Declined:

@up-tandem
Copy link
Contributor Author

Review: Inline video playback required

The current implementation uses Linking.openURL(trimmedCtaLink) for the CTA, but the VideoHeroRenderer also needs inline video playback for the streamingUrl field — the hero should play video directly on the page, not just show a static thumbnail.

What needs to change:

  1. Add expo-video dependency (npx expo install expo-video)
  2. Replace the static ImageBackground thumbnail with a VideoView from expo-video that plays the streamingUrl (HLS .m3u8)
  3. Use the video thumbnail (video.image.url) as the poster image before playback starts
  4. Add play/pause controls overlaid on the video area
  5. The CTA button (ctaLabel/ctaLink) remains separate — it's a navigation action, not the video play trigger

Reference:

The heading, subheading, CTA, and fallback behavior all look good — just needs the inline video player added.

Replace static ImageBackground with expo-video VideoView for inline
HLS streaming. Add streamingUrl field to VideoHeroSection model.
Includes play/pause overlay controls, fallback to thumbnail when
streamingUrl is null, and global jest.setup.js for expo-video mocks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@up-tandem
Copy link
Contributor Author

Review feedback addressed (ab09c82)

Handled:

  • [Inline video playback required]: Replaced static ImageBackground with expo-video VideoView for inline HLS streaming. Added streamingUrl field to VideoHeroSection model. Video plays inline with play/pause overlay controls. Falls back to static thumbnail when streamingUrl is null. Added global jest.setup.js with expo-video mock for all tests.

Changes:

  • sectionModels.ts: Added streamingUrl: string | null to VideoHeroSection
  • VideoHeroRenderer.tsx: Uses useVideoPlayer + VideoView for inline playback, play/pause overlay, thumbnail fallback
  • jest.setup.js: Global mock for expo-video native module
  • package.json: Added expo-video ~3.0.16 dependency, jest setupFiles config
  • Test fixtures updated with new streamingUrl field

Copy link

@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: 2

Caution

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

⚠️ Outside diff range comments (1)
mobile/expo/src/lib/sectionModels.ts (1)

80-89: ⚠️ Potential issue | 🔴 Critical

Map streamingUrl in sectionMapper or the new hero player never renders.

This interface now requires streamingUrl, but mobile/expo/src/lib/sectionMapper.ts:1-20 still builds VideoHeroSection without it. In production that leaves streamingUrl undefined, so VideoHeroRenderer always takes the non-video branch and the inline HLS path added in this PR never activates.

🐛 Proposed fix
 function mapVideoHero(
   raw: RawSection & { __typename: "ComponentSectionsVideoHero" },
 ): VideoHeroSection {
   return {
     kind: "videoHero",
     id: raw.id,
     sectionKey: raw.sectionKey ?? null,
     heading: raw.videoHeroHeading ?? null,
     subheading: raw.subheading ?? null,
+    streamingUrl: raw.streamingUrl ?? null,
     ctaLink: raw.ctaLink ?? null,
     ctaLabel: raw.ctaLabel ?? null,
     video: mapVideoModel(raw.heroVideo),
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/expo/src/lib/sectionModels.ts` around lines 80 - 89, The
VideoHeroSection interface now requires streamingUrl but sectionMapper still
constructs VideoHeroSection without it, causing VideoHeroRenderer to take the
non-video branch; update the sectionMapper function that builds VideoHeroSection
to populate the streamingUrl field (use the source object's streaming URL or
null fallback) so VideoHeroSection.streamingUrl is defined when appropriate and
the inline HLS path in VideoHeroRenderer can activate; ensure you reference the
same property name (streamingUrl) and preserve other fields (video, ctaLink,
ctaLabel, heading, subheading, sectionKey, id).
🧹 Nitpick comments (1)
mobile/expo/jest.setup.js (1)

1-4: Run the useVideoPlayer setup callback in the mock.

VideoHeroRenderer configures the player in the hook callback (p.loop = false), but this mock never invokes that callback. Tests will stay green even if the initialization path regresses.

♻️ Proposed fix
 jest.mock("expo-video", () => ({
-  useVideoPlayer: () => ({ play: jest.fn(), pause: jest.fn() }),
+  useVideoPlayer: (_source, setup) => {
+    const player = { play: jest.fn(), pause: jest.fn(), loop: false }
+    setup?.(player)
+    return player
+  },
   VideoView: "VideoView",
 }))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/expo/jest.setup.js` around lines 1 - 4, The mock for expo-video does
not invoke the setup callback passed to useVideoPlayer, so VideoHeroRenderer's
initialization (e.g., setting p.loop = false) never runs in tests; update the
jest.mock for "expo-video" so useVideoPlayer accepts and calls the provided
setup callback with a player object (containing play, pause, loop, etc.) before
returning control, ensuring the hook's initializer in VideoHeroRenderer is
exercised during tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mobile/expo/src/components/sections/VideoHeroRenderer.tsx`:
- Around line 48-73: When streamingUrl is present we currently render VideoView
immediately so the poster never shows; update VideoHeroRenderer to display
thumbnailUrl as a visible poster before playback by rendering a thumbnail Image
(or ImageBackground) when !isPlaying and thumbnailUrl exists, and hide it once
playback starts (isPlaying true). Keep existing VideoView, Pressable,
handlePlayPress/handlePausePress and play/pause overlay, but conditionally show
the thumbnail (above the VideoView or by toggling VideoView opacity/visibility)
so users see the poster while the stream buffers; reference streamingUrl,
thumbnailUrl, isPlaying, VideoView, Pressable, handlePlayPress,
handlePausePress, styles.playPauseOverlay, styles.playButton, and
styles.playIcon when making the change.
- Around line 21-23: The component currently passes an empty string to
useVideoPlayer when streamingUrl is missing; change the initial source argument
to null (i.e., call useVideoPlayer(streamingUrl ?? null, ...)) so the player
doesn't treat "" as an invalid URI, and when a stream appears call
player.replaceAsync or player.replace to load the media; update references
around useVideoPlayer, player, and streamingUrl to implement this change.

---

Outside diff comments:
In `@mobile/expo/src/lib/sectionModels.ts`:
- Around line 80-89: The VideoHeroSection interface now requires streamingUrl
but sectionMapper still constructs VideoHeroSection without it, causing
VideoHeroRenderer to take the non-video branch; update the sectionMapper
function that builds VideoHeroSection to populate the streamingUrl field (use
the source object's streaming URL or null fallback) so
VideoHeroSection.streamingUrl is defined when appropriate and the inline HLS
path in VideoHeroRenderer can activate; ensure you reference the same property
name (streamingUrl) and preserve other fields (video, ctaLink, ctaLabel,
heading, subheading, sectionKey, id).

---

Nitpick comments:
In `@mobile/expo/jest.setup.js`:
- Around line 1-4: The mock for expo-video does not invoke the setup callback
passed to useVideoPlayer, so VideoHeroRenderer's initialization (e.g., setting
p.loop = false) never runs in tests; update the jest.mock for "expo-video" so
useVideoPlayer accepts and calls the provided setup callback with a player
object (containing play, pause, loop, etc.) before returning control, ensuring
the hook's initializer in VideoHeroRenderer is exercised during tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 00133f7b-631d-426b-bfee-0fbf7080c974

📥 Commits

Reviewing files that changed from the base of the PR and between f1bd2f6 and ab09c82.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • mobile/expo/app.json
  • mobile/expo/jest.setup.js
  • mobile/expo/package.json
  • mobile/expo/src/components/sections/SectionDispatcher.test.tsx
  • mobile/expo/src/components/sections/VideoHeroRenderer.test.tsx
  • mobile/expo/src/components/sections/VideoHeroRenderer.tsx
  • mobile/expo/src/lib/sectionModels.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • mobile/expo/src/components/sections/VideoHeroRenderer.test.tsx

Comment on lines +21 to +23
const player = useVideoPlayer(streamingUrl ?? "", (p) => {
p.loop = false
})
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For the official expo-video useVideoPlayerAPI, should the source benull rather than an empty string when no media should be loaded yet?

💡 Result:

Yes—per the official expo-video docs, if you don’t want any media loaded yet, initialize the player with a null source (then later load media by replacing the source via replaceAsync / replace). Passing an empty string would be treated like a (invalid) URI rather than “no source.” [1]

Source:
[1] Expo Documentation – expo-video (“Preloading videos” section, mentions creating a VideoPlayer with a null source)


Use null, not "", for the no-stream player source.

When streamingUrl is absent, the component passes an empty string to useVideoPlayer instead of null. According to the official expo-video documentation, an empty string is treated as an (invalid) URI. Instead, pass null to the player when no media should be loaded yet, then use replaceAsync or replace to load media later.

Proposed fix
-  const player = useVideoPlayer(streamingUrl ?? "", (p) => {
+  const player = useVideoPlayer(streamingUrl, (p) => {
     p.loop = false
   })
📝 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 player = useVideoPlayer(streamingUrl ?? "", (p) => {
p.loop = false
})
const player = useVideoPlayer(streamingUrl, (p) => {
p.loop = false
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/expo/src/components/sections/VideoHeroRenderer.tsx` around lines 21 -
23, The component currently passes an empty string to useVideoPlayer when
streamingUrl is missing; change the initial source argument to null (i.e., call
useVideoPlayer(streamingUrl ?? null, ...)) so the player doesn't treat "" as an
invalid URI, and when a stream appears call player.replaceAsync or
player.replace to load the media; update references around useVideoPlayer,
player, and streamingUrl to implement this change.

Comment on lines +48 to +73
{streamingUrl ? (
<>
{/* @ts-expect-error React 19 vs RN component types */}
<VideoView
player={player}
style={StyleSheet.absoluteFill}
nativeControls={false}
contentFit="cover"
/>
{/* Play/pause overlay */}
{/* @ts-expect-error React 19 vs RN component types */}
<Pressable
style={styles.playPauseOverlay}
onPress={isPlaying ? handlePausePress : handlePlayPress}
accessibilityRole="button"
accessibilityLabel={isPlaying ? "Pause video" : "Play video"}
>
{!isPlaying && (
// @ts-expect-error React 19 vs RN component types
<View style={styles.playButton}>
{/* @ts-expect-error RN Text vs React 19 ReactNode */}
<Text style={styles.playIcon}>▶</Text>
</View>
)}
</Pressable>
</>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The thumbnail never appears before playback starts.

When streamingUrl exists, this branch renders VideoView immediately and never uses thumbnailUrl. That misses the poster/placeholder requirement from issue #306 and can leave users staring at a blank hero while the stream buffers.

🐛 Proposed fix
-  const [isPlaying, setIsPlaying] = useState(false)
+  const [isPlaying, setIsPlaying] = useState(false)
+  const [hasStarted, setHasStarted] = useState(false)
@@
   const handlePlayPress = () => {
     if (player) {
       player.play()
+      setHasStarted(true)
       setIsPlaying(true)
     }
   }
@@
       {streamingUrl ? (
         <>
           {/* `@ts-expect-error` React 19 vs RN component types */}
           <VideoView
             player={player}
             style={StyleSheet.absoluteFill}
             nativeControls={false}
             contentFit="cover"
           />
+          {!hasStarted && thumbnailUrl ? (
+            // `@ts-expect-error` React 19 vs RN component types
+            <Image
+              source={{ uri: thumbnailUrl }}
+              style={StyleSheet.absoluteFill}
+              resizeMode="cover"
+              accessibilityLabel={
+                video.image?.alternativeText ?? `${video.title} thumbnail`
+              }
+            />
+          ) : null}
           {/* Play/pause overlay */}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/expo/src/components/sections/VideoHeroRenderer.tsx` around lines 48 -
73, When streamingUrl is present we currently render VideoView immediately so
the poster never shows; update VideoHeroRenderer to display thumbnailUrl as a
visible poster before playback by rendering a thumbnail Image (or
ImageBackground) when !isPlaying and thumbnailUrl exists, and hide it once
playback starts (isPlaying true). Keep existing VideoView, Pressable,
handlePlayPress/handlePausePress and play/pause overlay, but conditionally show
the thumbnail (above the VideoView or by toggling VideoView opacity/visibility)
so users see the poster while the stream buffers; reference streamingUrl,
thumbnailUrl, isPlaying, VideoView, Pressable, handlePlayPress,
handlePausePress, styles.playPauseOverlay, styles.playButton, and
styles.playIcon when making the change.

@up-tandem
Copy link
Contributor Author

Test Report — PR #321 (VideoHeroRenderer)

CI Status

All checks passing.

Local Checks

Check Result
Lint Pass (after fix: added jest globals to eslint config)
Tests 42/42 passing
Typecheck Pass for PR files (remaining errors pre-existing in stubs/sectionMapper)
iOS build Build succeeded, app runs without crash
Android build Build succeeded, app runs without crash

Fixes Applied

  1. mobile/expo/eslint.config.js — added jest global for test files (jest.setup.js, *.test.ts, *.test.tsx)
  2. mobile/expo/src/components/sections/VideoHeroRenderer.tsx — removed 13 unused @ts-expect-error directives (types resolved)
  3. mobile/expo/src/components/sections/SectionDispatcher.tsx — removed 2 unused @ts-expect-error directives
  4. packages/graphql/src/watchExperience.ts — added ComponentSectionsVideoHero inline fragment to GET_WATCH_EXPERIENCE query (root cause of runtime crash: Cannot read property 'documentId' of undefined)
  5. mobile/expo/src/lib/sectionMapper.ts — added missing streamingUrl field in mapVideoHero

iOS Screenshot (live data from Strapi)

VideoHeroRenderer renders correctly with live API data:

  • Heading: "Easter"
  • Subheading: "Easter 2025"
  • CTA button: "Watch now" → links to configured URL
  • Video streaming URL resolved (mux.dev test stream)
  • Thumbnail/fallback renders when streamingUrl is null
  • Dark overlay for text readability

Notes

  • The E2E verification screen now loads 4 sections from Strapi without crashing (videoHero, bibleQuotesCarousel, cta, text)
  • Pre-existing typecheck errors in stub files and sectionMapper.ts remain (will be resolved as each renderer PR lands its query fragment)

🤖 Generated with Claude Code

… issues

Add ComponentSectionsVideoHero inline fragment to GET_WATCH_EXPERIENCE
query to resolve runtime crash (Cannot read property 'documentId' of
undefined). Also add missing streamingUrl mapping, jest globals for
eslint, and remove unused @ts-expect-error directives.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@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.

♻️ Duplicate comments (2)
mobile/expo/src/components/sections/VideoHeroRenderer.tsx (2)

47-68: ⚠️ Potential issue | 🟠 Major

Poster/thumbnail not shown before playback starts.

When streamingUrl exists, VideoView renders immediately without displaying the thumbnail. Per issue #306, the video entity's image should be used as a "poster/thumbnail before playback." Currently, thumbnailUrl is computed but never used in this branch, leaving users with a potentially blank hero while the stream buffers.

Proposed fix
+  const [hasStarted, setHasStarted] = useState(false)
+
   const handlePlayPress = () => {
     if (player) {
       player.play()
+      setHasStarted(true)
       setIsPlaying(true)
     }
   }
@@
       {streamingUrl ? (
         <>
-          <VideoView
-            player={player}
-            style={StyleSheet.absoluteFill}
-            nativeControls={false}
-            contentFit="cover"
-          />
+          {hasStarted ? (
+            <VideoView
+              player={player}
+              style={StyleSheet.absoluteFill}
+              nativeControls={false}
+              contentFit="cover"
+            />
+          ) : thumbnailUrl ? (
+            <Image
+              source={{ uri: thumbnailUrl }}
+              style={StyleSheet.absoluteFill}
+              resizeMode="cover"
+              accessibilityLabel={
+                video.image?.alternativeText ?? `${video.title} thumbnail`
+              }
+            />
+          ) : (
+            <View style={[StyleSheet.absoluteFill, styles.fallbackBackground]} />
+          )}
           {/* Play/pause overlay */}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/expo/src/components/sections/VideoHeroRenderer.tsx` around lines 47 -
68, The VideoView renders immediately when streamingUrl exists and never shows
the computed thumbnailUrl; update VideoHeroRenderer to display the thumbnail
before playback by rendering the thumbnail Image beneath (or as a background to)
the VideoView when streamingUrl is truthy and isPlaying is false (use
thumbnailUrl as the Image source and StyleSheet.absoluteFill so it fills the
hero). Ensure the Image is hidden or removed once playback starts (tied to
isPlaying or video-ready state) and keep the existing Play/pause overlay and
handlers (handlePlayPress, handlePausePress) intact so tapping plays the video
and hides the poster.

21-23: ⚠️ Potential issue | 🟠 Major

Use null, not "", for the no-stream player source.

The expo-video useVideoPlayer hook accepts null as a valid source type for when no media is loaded yet. An empty string would be treated as an invalid URI. Initialize with null and use replaceAsync() to load the source when available.

Proposed fix
-  const player = useVideoPlayer(streamingUrl ?? "", (p) => {
+  const player = useVideoPlayer(streamingUrl ?? null, (p) => {
     p.loop = false
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/expo/src/components/sections/VideoHeroRenderer.tsx` around lines 21 -
23, Change the useVideoPlayer initialization to pass null instead of an empty
string so the hook treats it as no source: call useVideoPlayer(null, (p) => {
p.loop = false }) and when streamingUrl becomes available, load it via
player.replaceAsync or equivalent (reference symbols: useVideoPlayer,
player.replaceAsync, streamingUrl) to set the media source dynamically.
🧹 Nitpick comments (1)
mobile/expo/src/components/sections/VideoHeroRenderer.tsx (1)

31-43: Synchronize isPlaying state with player status events.

The isPlaying state is managed manually via handlePlayPress/handlePausePress but doesn't sync with actual player state. If the video ends, buffers, or encounters an error, isPlaying will be stale. Use expo's useEvent hook to listen to the player's statusChange event (or playingChange if available):

import { useEvent } from 'expo';

const { status } = useEvent(player, 'statusChange', {
  status: player.status,
});

// Update isPlaying based on status.isPlaying or player.playing
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/expo/src/components/sections/VideoHeroRenderer.tsx` around lines 31 -
43, The isPlaying state set only in handlePlayPress/handlePausePress can become
stale; wire it to the player's real status by subscribing to player events
(useEvent) and updating setIsPlaying from the event payload (e.g.,
status.isPlaying or status.playing). Add a useEvent subscription for player with
the 'statusChange' (or 'playingChange') event, read the status boolean and call
setIsPlaying accordingly, and ensure you still keep
handlePlayPress/handlePausePress for user-initiated play/pause but remove any
conflicting manual state updates so state always reflects player status.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@mobile/expo/src/components/sections/VideoHeroRenderer.tsx`:
- Around line 47-68: The VideoView renders immediately when streamingUrl exists
and never shows the computed thumbnailUrl; update VideoHeroRenderer to display
the thumbnail before playback by rendering the thumbnail Image beneath (or as a
background to) the VideoView when streamingUrl is truthy and isPlaying is false
(use thumbnailUrl as the Image source and StyleSheet.absoluteFill so it fills
the hero). Ensure the Image is hidden or removed once playback starts (tied to
isPlaying or video-ready state) and keep the existing Play/pause overlay and
handlers (handlePlayPress, handlePausePress) intact so tapping plays the video
and hides the poster.
- Around line 21-23: Change the useVideoPlayer initialization to pass null
instead of an empty string so the hook treats it as no source: call
useVideoPlayer(null, (p) => { p.loop = false }) and when streamingUrl becomes
available, load it via player.replaceAsync or equivalent (reference symbols:
useVideoPlayer, player.replaceAsync, streamingUrl) to set the media source
dynamically.

---

Nitpick comments:
In `@mobile/expo/src/components/sections/VideoHeroRenderer.tsx`:
- Around line 31-43: The isPlaying state set only in
handlePlayPress/handlePausePress can become stale; wire it to the player's real
status by subscribing to player events (useEvent) and updating setIsPlaying from
the event payload (e.g., status.isPlaying or status.playing). Add a useEvent
subscription for player with the 'statusChange' (or 'playingChange') event, read
the status boolean and call setIsPlaying accordingly, and ensure you still keep
handlePlayPress/handlePausePress for user-initiated play/pause but remove any
conflicting manual state updates so state always reflects player status.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f845cd2e-aa60-42d9-9bf8-23e2bb8f5b51

📥 Commits

Reviewing files that changed from the base of the PR and between ab09c82 and 583218a.

📒 Files selected for processing (5)
  • mobile/expo/eslint.config.js
  • mobile/expo/src/components/sections/SectionDispatcher.tsx
  • mobile/expo/src/components/sections/VideoHeroRenderer.tsx
  • mobile/expo/src/lib/sectionMapper.ts
  • packages/graphql/src/watchExperience.ts

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.

feat(mobile-expo): VideoHeroRenderer section component

2 participants