about summary refs log tree commit diff
path: root/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2025-06-13 12:05:41 -0500
committerGitHub <noreply@github.com>2025-06-13 12:05:41 -0500
commit45f0f7eefecae1922c2f30d4e7760d2b93b1ae56 (patch)
treea2fd6917867f18fe334b54dd3289775c2930bc85 /src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx
parentba0f5a9bdef5bd0447ded23cab1af222b65511cc (diff)
downloadvoidsky-45f0f7eefecae1922c2f30d4e7760d2b93b1ae56.tar.zst
Port post embeds to new arch (#7408)
* Direct port of embeds to new arch

(cherry picked from commit cc3fa1f6cea396dd9222486c633a508bfee1ecd6)

* Re-org

* Split out ListEmbed and FeedEmbed

* Split out ImageEmbed

* DRY up a bit

* Port over ExternalLinkEmbed

* Port over Player and Gif embeds

* Migrate ComposerReplyTo

* Replace other usages of old post-embeds

* Migrate view contexts

* Copy pasta VideoEmbed

* Copy pasta GifEmbed

* Swap in new file location

* Clean up

* Fix up native

* Add back in correct moderation on List and Feed embeds

* Format

* Prettier

* delete old video utils

* move bandwidth-estimate.ts

* Remove log

* Add LazyQuoteEmbed for composer use

* Clean up unused things

* Remove remaining items

* Prettier

* Fix imports

* Handle nested quotes same as prod

* Add back silenced error handling

* Fix lint

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Diffstat (limited to 'src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx')
-rw-r--r--src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx210
1 files changed, 210 insertions, 0 deletions
diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx
new file mode 100644
index 000000000..88879d45a
--- /dev/null
+++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx
@@ -0,0 +1,210 @@
+import React, {useRef} from 'react'
+import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
+import {AppBskyEmbedVideo} from '@atproto/api'
+import {BlueskyVideoView} from '@haileyok/bluesky-video'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {HITSLOP_30} from '#/lib/constants'
+import {useAutoplayDisabled} from '#/state/preferences'
+import {atoms as a, useTheme} from '#/alf'
+import {useIsWithinMessage} from '#/components/dms/MessageContext'
+import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
+import {Pause_Filled_Corner0_Rounded as PauseIcon} from '#/components/icons/Pause'
+import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
+import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
+import {MediaInsetBorder} from '#/components/MediaInsetBorder'
+import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
+import {TimeIndicator} from './TimeIndicator'
+
+export const VideoEmbedInnerNative = React.forwardRef(
+  function VideoEmbedInnerNative(
+    {
+      embed,
+      setStatus,
+      setIsLoading,
+      setIsActive,
+    }: {
+      embed: AppBskyEmbedVideo.View
+      setStatus: (status: 'playing' | 'paused') => void
+      setIsLoading: (isLoading: boolean) => void
+      setIsActive: (isActive: boolean) => void
+    },
+    ref: React.Ref<{togglePlayback: () => void}>,
+  ) {
+    const {_} = useLingui()
+    const videoRef = useRef<BlueskyVideoView>(null)
+    const autoplayDisabled = useAutoplayDisabled()
+    const isWithinMessage = useIsWithinMessage()
+    const [muted, setMuted] = useVideoMuteState()
+
+    const [isPlaying, setIsPlaying] = React.useState(false)
+    const [timeRemaining, setTimeRemaining] = React.useState(0)
+    const [error, setError] = React.useState<string>()
+
+    React.useImperativeHandle(ref, () => ({
+      togglePlayback: () => {
+        videoRef.current?.togglePlayback()
+      },
+    }))
+
+    if (error) {
+      throw new Error(error)
+    }
+
+    return (
+      <View style={[a.flex_1, a.relative]}>
+        <BlueskyVideoView
+          url={embed.playlist}
+          autoplay={!autoplayDisabled && !isWithinMessage}
+          beginMuted={autoplayDisabled ? false : muted}
+          style={[a.rounded_sm]}
+          onActiveChange={e => {
+            setIsActive(e.nativeEvent.isActive)
+          }}
+          onLoadingChange={e => {
+            setIsLoading(e.nativeEvent.isLoading)
+          }}
+          onMutedChange={e => {
+            setMuted(e.nativeEvent.isMuted)
+          }}
+          onStatusChange={e => {
+            setStatus(e.nativeEvent.status)
+            setIsPlaying(e.nativeEvent.status === 'playing')
+          }}
+          onTimeRemainingChange={e => {
+            setTimeRemaining(e.nativeEvent.timeRemaining)
+          }}
+          onError={e => {
+            setError(e.nativeEvent.error)
+          }}
+          ref={videoRef}
+          accessibilityLabel={
+            embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`)
+          }
+          accessibilityHint=""
+        />
+        <VideoControls
+          enterFullscreen={() => {
+            videoRef.current?.enterFullscreen(true)
+          }}
+          toggleMuted={() => {
+            videoRef.current?.toggleMuted()
+          }}
+          togglePlayback={() => {
+            videoRef.current?.togglePlayback()
+          }}
+          isPlaying={isPlaying}
+          timeRemaining={timeRemaining}
+        />
+        <MediaInsetBorder />
+      </View>
+    )
+  },
+)
+
+function VideoControls({
+  enterFullscreen,
+  toggleMuted,
+  togglePlayback,
+  timeRemaining,
+  isPlaying,
+}: {
+  enterFullscreen: () => void
+  toggleMuted: () => void
+  togglePlayback: () => void
+  timeRemaining: number
+  isPlaying: boolean
+}) {
+  const {_} = useLingui()
+  const t = useTheme()
+  const [muted] = useVideoMuteState()
+
+  // show countdown when:
+  // 1. timeRemaining is a number - was seeing NaNs
+  // 2. duration is greater than 0 - means metadata has loaded
+  // 3. we're less than 5 second into the video
+  const showTime = !isNaN(timeRemaining)
+
+  return (
+    <View style={[a.absolute, a.inset_0]}>
+      <Pressable
+        onPress={enterFullscreen}
+        style={a.flex_1}
+        accessibilityLabel={_(msg`Video`)}
+        accessibilityHint={_(msg`Enters full screen`)}
+        accessibilityRole="button"
+      />
+      <ControlButton
+        onPress={togglePlayback}
+        label={isPlaying ? _(msg`Pause`) : _(msg`Play`)}
+        accessibilityHint={_(msg`Plays or pauses the video`)}
+        style={{left: 6}}>
+        {isPlaying ? (
+          <PauseIcon width={13} fill={t.palette.white} />
+        ) : (
+          <PlayIcon width={13} fill={t.palette.white} />
+        )}
+      </ControlButton>
+      {showTime && <TimeIndicator time={timeRemaining} style={{left: 33}} />}
+
+      <ControlButton
+        onPress={toggleMuted}
+        label={
+          muted
+            ? _(msg({message: `Unmute`, context: 'video'}))
+            : _(msg({message: `Mute`, context: 'video'}))
+        }
+        accessibilityHint={_(msg`Toggles the sound`)}
+        style={{right: 6}}>
+        {muted ? (
+          <MuteIcon width={13} fill={t.palette.white} />
+        ) : (
+          <UnmuteIcon width={13} fill={t.palette.white} />
+        )}
+      </ControlButton>
+    </View>
+  )
+}
+
+function ControlButton({
+  onPress,
+  children,
+  label,
+  accessibilityHint,
+  style,
+}: {
+  onPress: () => void
+  children: React.ReactNode
+  label: string
+  accessibilityHint: string
+  style?: StyleProp<ViewStyle>
+}) {
+  return (
+    <View
+      style={[
+        a.absolute,
+        a.rounded_full,
+        a.justify_center,
+        {
+          backgroundColor: 'rgba(0, 0, 0, 0.5)',
+          paddingHorizontal: 4,
+          paddingVertical: 4,
+          bottom: 6,
+          minHeight: 21,
+          minWidth: 21,
+        },
+        style,
+      ]}>
+      <Pressable
+        onPress={onPress}
+        style={a.flex_1}
+        accessibilityLabel={label}
+        accessibilityHint={accessibilityHint}
+        accessibilityRole="button"
+        hitSlop={HITSLOP_30}>
+        {children}
+      </Pressable>
+    </View>
+  )
+}