Skip to content

feat(mobile-expo): VideoRenderer section component#324

Merged
up-tandem merged 4 commits intomainfrom
feat/309-video-renderer
Mar 10, 2026
Merged

feat(mobile-expo): VideoRenderer section component#324
up-tandem merged 4 commits intomainfrom
feat/309-video-renderer

Conversation

@up-tandem
Copy link
Contributor

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

Summary

  • Replaces the Video stub renderer with a real implementation
  • Displays video thumbnail (from media or video.image) with 16:9 aspect ratio
  • Play button overlay on thumbnail; tapping opens streamingUrl via Linking.openURL
  • Title and subtitle below thumbnail
  • Fallback dark background when no thumbnail available
  • 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=VideoRenderer — 4/4 tests pass
  • pnpm --filter @forge/expo test -- --testPathPattern=SectionDispatcher — 11/11 tests pass (no regressions)

Resolves #309

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added video player with poster image fallback and manual play/pause controls
    • Enabled full-screen and picture-in-picture viewing options
    • Display video titles and subtitles alongside player
  • Tests

    • Added unit tests for video rendering component

Replace the Video stub with a real renderer that displays a video
thumbnail with play button overlay, title, and subtitle. Tapping
opens the streaming URL via Linking.

Resolves #309

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

coderabbitai bot commented Mar 10, 2026

Warning

Rate limit exceeded

@up-tandem has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 8 minutes and 42 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4b483892-1c2c-4f02-a455-4f688d1bf8d3

📥 Commits

Reviewing files that changed from the base of the PR and between 3cc093d and fa17416.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • mobile/expo/src/components/sections/SectionDispatcher.tsx
  • mobile/expo/src/components/sections/VideoRenderer.tsx
  • mobile/expo/src/components/sections/index.ts

Walkthrough

This pull request introduces a functional VideoRenderer component for inline video playback in mobile/expo. It replaces a stub with a real React Native implementation using expo-video, adds unit tests, configures Jest mocks, and updates Expo plugin and package dependencies.

Changes

Cohort / File(s) Summary
Configuration & Dependencies
app.json, package.json, jest.setup.js
Adds expo-video plugin to Expo config; adds expo-video (~3.0.16) dependency and configures Jest setupFiles; creates Jest mock for expo-video module with useVideoPlayer and VideoView mocks.
Component Implementation
src/components/sections/VideoRenderer.tsx, src/components/sections/VideoStub.tsx, src/components/sections/SectionDispatcher.tsx, src/components/sections/index.ts
Replaces VideoStub stub component with full VideoRenderer implementation featuring poster image display, play/pause state management, inline HLS playback via useVideoPlayer, native controls, fullscreen/PiP options, and title/subtitle rendering. Updates import path in SectionDispatcher and re-export in index.ts.
Tests
src/components/sections/VideoRenderer.test.tsx
Adds unit tests verifying VideoRenderer renders without errors across multiple scenarios: complete fields, minimal fields, missing media, and missing both media and video.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ 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%. 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): VideoRenderer section component' directly and clearly summarizes the main change: the introduction of a VideoRenderer component for the mobile-expo package.
Linked Issues check ✅ Passed The PR implements all acceptance criteria from issue #309: inline video playback with expo-video, poster image support, play/pause controls, title/subtitle rendering, proper field handling, stub replacement, and accessibility labels.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the VideoRenderer component per issue #309: video component, configuration updates, dependencies, mocks, tests, and import updates.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/309-video-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.

@up-tandem
Copy link
Contributor Author

Review: Inline video playback required — do not use Linking.openURL

The current implementation uses Linking.openURL(streamingUrl) which opens the video in the system player outside the app. Per updated issue #309, the VideoRenderer must play video inline on the same page.

What needs to change:

  1. Add expo-video dependency (npx expo install expo-video)
  2. Replace the Pressable + Linking.openURL with a VideoView from expo-video
  3. Use streamingUrl (HLS .m3u8) as the video source
  4. Use media.url or video.image.url as the poster/placeholder image
  5. 16:9 aspect ratio video player with native playback controls
  6. Tap-to-play: show poster with play button overlay, then start inline playback on tap

Example with expo-video:

import { useVideoPlayer, VideoView } from 'expo-video'

const player = useVideoPlayer(streamingUrl, (p) => {
  p.loop = false
})

<VideoView
  player={player}
  style={{ width: '100%', aspectRatio: 16 / 9 }}
  allowsFullscreen
  allowsPictureInPicture
/>

Reference:

The thumbnail fallback, title/subtitle rendering, and accessibility labels all look good — the main change is replacing Linking.openURL with inline expo-video playback.

Replace Linking.openURL with expo-video VideoView for inline HLS
streaming. Shows poster thumbnail with play button, tapping starts
inline playback with native controls. Includes fullscreen and
picture-in-picture support. Added 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 (3cc093d)

Handled:

  • [Inline video playback required — do not use Linking.openURL]: Replaced Linking.openURL(streamingUrl) with expo-video VideoView for inline HLS streaming. Shows poster thumbnail with play button overlay; tapping starts inline playback with native controls, fullscreen, and picture-in-picture support. Added expo-video ~3.0.16 dependency and global jest.setup.js for mocks.

Changes:

  • VideoRenderer.tsx: Uses useVideoPlayer + VideoView for inline playback with native controls, poster/thumbnail with play overlay before playback
  • jest.setup.js: Global mock for expo-video native module
  • package.json: Added expo-video ~3.0.16 dependency, jest setupFiles config

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: 1

🧹 Nitpick comments (3)
mobile/expo/jest.setup.js (1)

1-4: Mock VideoView as a component instead of a string.

The current mock returns "VideoView" as a string. While this works for the current tests (which only call createElement without rendering), it will break if future tests actually render the component and trigger the isPlaying=true branch—React cannot render a plain string as a component.

♻️ Proposed fix
 jest.mock("expo-video", () => ({
   useVideoPlayer: () => ({ play: jest.fn(), pause: jest.fn() }),
-  VideoView: "VideoView",
+  VideoView: ({ children, ...props }) => children ?? null,
 }))
🤖 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
returns VideoView as a string which cannot be rendered; update the jest.mock to
export VideoView as a React component (e.g., a functional component or
forwardRef) instead of a string and ensure it accepts props used by code under
test (such as isPlaying and any children) so rendering and prop-branch logic
will work; keep useVideoPlayer mocked as before and ensure the test setup
imports React if required so the mocked component can return a valid React
element.
mobile/expo/src/components/sections/VideoRenderer.test.tsx (1)

28-67: Consider expanding test coverage beyond "doesn't throw" checks.

The current tests only verify that createElement doesn't throw, which confirms the component can be instantiated but doesn't validate:

  • Actual rendering output (thumbnail, play button, title/subtitle)
  • User interactions (play/pause behavior)
  • Accessibility attributes are correctly applied

This is a reasonable starting point, but consider adding render-based tests using @testing-library/react-native for more robust coverage:

♻️ Example expanded test
import { render, fireEvent } from "@testing-library/react-native"

it("renders title and subtitle when provided", () => {
  const { getByText } = render(<VideoRenderer section={baseSection} />)
  expect(getByText("Jesus' Victory Over Sin and Death")).toBeTruthy()
  expect(getByText("Watch the full story")).toBeTruthy()
})

it("renders play button with accessible label", () => {
  const { getByLabelText } = render(<VideoRenderer section={baseSection} />)
  expect(getByLabelText("Play Jesus' Victory Over Sin and Death")).toBeTruthy()
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/expo/src/components/sections/VideoRenderer.test.tsx` around lines 28 -
67, Replace the current "doesn't throw" instantiation tests in
VideoRenderer.test.tsx with render-based assertions using
`@testing-library/react-native`: import render and fireEvent, render
<VideoRenderer section={baseSection} /> and assert visible text for
title/subtitle (use getByText), assert thumbnail and play button presence and
accessible label (use getByLabelText or getByA11yLabel), and add an interaction
test using fireEvent.press to simulate play/pause and verify UI/state changes;
reference the VideoRenderer component and the baseSection/minimal section
objects to create the different test cases (full, minimal, fallback thumbnail,
no-thumb).
mobile/expo/src/components/sections/VideoRenderer.tsx (1)

17-35: Sync isPlaying state with actual player status using Expo's useEvent hook.

The isPlaying state is managed independently from the actual player state. If a user pauses via native controls (enabled on line 48), isPlaying remains true and the poster won't reappear. Similarly, when the video ends naturally, the state won't reset.

Use Expo's useEvent hook to automatically synchronize state with player events:

♻️ Recommended approach with useEvent
import { useEvent } from 'expo'
// ...

const { isPlaying } = useEvent(player, 'playingChange', { isPlaying: false })

This hook manages the event subscription and cleanup automatically, keeping your UI in sync with the actual player state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/expo/src/components/sections/VideoRenderer.tsx` around lines 17 - 35,
The local isPlaying state is out of sync with the real player when users use
native controls or when playback ends; replace the manual state management
(setIsPlaying in handlePlayPress/handlePausePress) by subscribing to the
player's playing state via Expo's useEvent so UI follows the player's actual
'playingChange' events (use the existing player from useVideoPlayer and remove
or stop relying on manual setIsPlaying). Ensure you import useEvent from 'expo',
derive isPlaying from the event subscription (keep the variable name isPlaying),
and remove any redundant state updates to avoid conflicting sources of truth.
🤖 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/VideoRenderer.tsx`:
- Around line 53-59: The pause overlay Pressable (style pauseOverlay) is
covering the entire video (absoluteFillObject) and intercepting nativeControls
touches; update the Pressable so it no longer blocks underlying native controls
by either removing the overlay or changing its touch behavior—recommended
change: remove absoluteFillObject from styles.pauseOverlay and add
pointerEvents="box-none" to the Pressable (keep onPress handlePausePress) so
taps on nativeControls are passed through; alternatively, if you prefer removing
manual overlay entirely, delete the Pressable that references pauseOverlay and
handlePausePress.

---

Nitpick comments:
In `@mobile/expo/jest.setup.js`:
- Around line 1-4: The mock for expo-video returns VideoView as a string which
cannot be rendered; update the jest.mock to export VideoView as a React
component (e.g., a functional component or forwardRef) instead of a string and
ensure it accepts props used by code under test (such as isPlaying and any
children) so rendering and prop-branch logic will work; keep useVideoPlayer
mocked as before and ensure the test setup imports React if required so the
mocked component can return a valid React element.

In `@mobile/expo/src/components/sections/VideoRenderer.test.tsx`:
- Around line 28-67: Replace the current "doesn't throw" instantiation tests in
VideoRenderer.test.tsx with render-based assertions using
`@testing-library/react-native`: import render and fireEvent, render
<VideoRenderer section={baseSection} /> and assert visible text for
title/subtitle (use getByText), assert thumbnail and play button presence and
accessible label (use getByLabelText or getByA11yLabel), and add an interaction
test using fireEvent.press to simulate play/pause and verify UI/state changes;
reference the VideoRenderer component and the baseSection/minimal section
objects to create the different test cases (full, minimal, fallback thumbnail,
no-thumb).

In `@mobile/expo/src/components/sections/VideoRenderer.tsx`:
- Around line 17-35: The local isPlaying state is out of sync with the real
player when users use native controls or when playback ends; replace the manual
state management (setIsPlaying in handlePlayPress/handlePausePress) by
subscribing to the player's playing state via Expo's useEvent so UI follows the
player's actual 'playingChange' events (use the existing player from
useVideoPlayer and remove or stop relying on manual setIsPlaying). Ensure you
import useEvent from 'expo', derive isPlaying from the event subscription (keep
the variable name isPlaying), and remove any redundant state updates to avoid
conflicting sources of truth.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4434c145-d81b-43db-a2b0-991f6f4a33b9

📥 Commits

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

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • mobile/expo/app.json
  • mobile/expo/jest.setup.js
  • mobile/expo/package.json
  • mobile/expo/src/components/sections/SectionDispatcher.tsx
  • mobile/expo/src/components/sections/VideoRenderer.test.tsx
  • mobile/expo/src/components/sections/VideoRenderer.tsx
  • mobile/expo/src/components/sections/VideoStub.tsx
  • mobile/expo/src/components/sections/index.ts
💤 Files with no reviewable changes (1)
  • mobile/expo/src/components/sections/VideoStub.tsx

sevenuphome and others added 2 commits March 11, 2026 11:39
# Conflicts:
#	mobile/expo/jest.setup.js
#	mobile/expo/src/components/sections/SectionDispatcher.tsx
#	mobile/expo/src/components/sections/index.ts
…deoRenderer

Replace manual play/pause toggle with poster-over-VideoView pattern.
VideoView with nativeControls is always rendered; poster overlay hides
once playback starts, letting native controls handle pause/seek/fullscreen.

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

Review feedback addressed (fa17416)

Handled:

  • [CodeRabbit: pause overlay blocks native controls]: Removed manual pause overlay entirely. VideoView with nativeControls is always rendered; poster hides on play, letting native controls handle pause/seek/fullscreen/PiP.

Test results

Check Result
CI pass
Lint pass
Tests 61 passed, 7 suites
iOS verified — thumbnail poster, play button, no-thumbnail placeholder all render correctly
Android verified — same

@up-tandem up-tandem merged commit 7c84b7d into main Mar 10, 2026
23 checks passed
@up-tandem up-tandem deleted the feat/309-video-renderer branch March 10, 2026 22:55
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): VideoRenderer section component

2 participants