about summary refs log tree commit diff
path: root/src/view/com/util
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-08-07 14:45:06 -0700
committerGitHub <noreply@github.com>2024-08-07 14:45:06 -0700
commit1b02f81cb85333462e3a9a42accc05d09aca4f2c (patch)
tree766e80438c1f109a1a7d751e9f04b7f6242f9766 /src/view/com/util
parentfff2c079c2554861764974aaeeb56f79a25ba82a (diff)
downloadvoidsky-1b02f81cb85333462e3a9a42accc05d09aca4f2c.tar.zst
[Video] Visibility detection view (#4741)
Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
Diffstat (limited to 'src/view/com/util')
-rw-r--r--src/view/com/util/List.tsx5
-rw-r--r--src/view/com/util/post-embeds/VideoEmbed.tsx47
-rw-r--r--src/view/com/util/post-embeds/VideoEmbed.web.tsx8
-rw-r--r--src/view/com/util/post-embeds/VideoEmbedInner.tsx143
-rw-r--r--src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx96
-rw-r--r--src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.web.tsx3
-rw-r--r--src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.native.tsx3
-rw-r--r--src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx (renamed from src/view/com/util/post-embeds/VideoEmbedInner.web.tsx)14
-rw-r--r--src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx (renamed from src/view/com/util/post-embeds/VideoWebControls.tsx)0
-rw-r--r--src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.web.tsx (renamed from src/view/com/util/post-embeds/VideoWebControls.web.tsx)4
10 files changed, 148 insertions, 175 deletions
diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx
index 9d9b1d802..c62ac5ed1 100644
--- a/src/view/com/util/List.tsx
+++ b/src/view/com/util/List.tsx
@@ -5,7 +5,9 @@ import {runOnJS, useSharedValue} from 'react-native-reanimated'
 import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
 import {usePalette} from '#/lib/hooks/usePalette'
 import {useScrollHandlers} from '#/lib/ScrollContext'
+import {useDedupe} from 'lib/hooks/useDedupe'
 import {addStyle} from 'lib/styles'
+import {updateActiveViewAsync} from '../../../../modules/expo-bluesky-swiss-army/src/VisibilityView'
 import {FlatList_INTERNAL} from './Views'
 
 export type ListMethods = FlatList_INTERNAL
@@ -47,6 +49,7 @@ function ListImpl<ItemT>(
 ) {
   const isScrolledDown = useSharedValue(false)
   const pal = usePalette('default')
+  const dedupe = useDedupe()
 
   function handleScrolledDownChange(didScrollDown: boolean) {
     onScrolledDownChange?.(didScrollDown)
@@ -77,6 +80,8 @@ function ListImpl<ItemT>(
           runOnJS(handleScrolledDownChange)(didScrollDown)
         }
       }
+
+      runOnJS(dedupe)(updateActiveViewAsync)
     },
     // Note: adding onMomentumBegin here makes simulator scroll
     // lag on Android. So either don't add it, or figure out why.
diff --git a/src/view/com/util/post-embeds/VideoEmbed.tsx b/src/view/com/util/post-embeds/VideoEmbed.tsx
index 429312d9e..887efac1a 100644
--- a/src/view/com/util/post-embeds/VideoEmbed.tsx
+++ b/src/view/com/util/post-embeds/VideoEmbed.tsx
@@ -1,21 +1,20 @@
-import React, {useCallback} from 'react'
+import React from 'react'
 import {View} from 'react-native'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
+import {VideoEmbedInnerNative} from 'view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative'
 import {atoms as a, useTheme} from '#/alf'
 import {Button, ButtonIcon} from '#/components/Button'
 import {Play_Filled_Corner2_Rounded as PlayIcon} from '#/components/icons/Play'
+import {VisibilityView} from '../../../../../modules/expo-bluesky-swiss-army'
 import {useActiveVideoView} from './ActiveVideoContext'
-import {VideoEmbedInner} from './VideoEmbedInner'
 
 export function VideoEmbed({source}: {source: string}) {
   const t = useTheme()
   const {active, setActive} = useActiveVideoView({source})
   const {_} = useLingui()
 
-  const onPress = useCallback(() => setActive(), [setActive])
-
   return (
     <View
       style={[
@@ -26,25 +25,27 @@ export function VideoEmbed({source}: {source: string}) {
         t.atoms.bg_contrast_25,
         a.my_xs,
       ]}>
-      {active ? (
-        <VideoEmbedInner
-          source={source}
-          // web only
-          active={active}
-          setActive={setActive}
-          onScreen={true}
-        />
-      ) : (
-        <Button
-          style={[a.flex_1, t.atoms.bg_contrast_25]}
-          onPress={onPress}
-          label={_(msg`Play video`)}
-          variant="ghost"
-          color="secondary"
-          size="large">
-          <ButtonIcon icon={PlayIcon} />
-        </Button>
-      )}
+      <VisibilityView
+        enabled={true}
+        onChangeStatus={isActive => {
+          if (isActive) {
+            setActive()
+          }
+        }}>
+        {active ? (
+          <VideoEmbedInnerNative />
+        ) : (
+          <Button
+            style={[a.flex_1, t.atoms.bg_contrast_25]}
+            onPress={setActive}
+            label={_(msg`Play video`)}
+            variant="ghost"
+            color="secondary"
+            size="large">
+            <ButtonIcon icon={PlayIcon} />
+          </Button>
+        )}
+      </VisibilityView>
     </View>
   )
 }
diff --git a/src/view/com/util/post-embeds/VideoEmbed.web.tsx b/src/view/com/util/post-embeds/VideoEmbed.web.tsx
index 08932f91f..70d887283 100644
--- a/src/view/com/util/post-embeds/VideoEmbed.web.tsx
+++ b/src/view/com/util/post-embeds/VideoEmbed.web.tsx
@@ -3,13 +3,15 @@ import {View} from 'react-native'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
+import {
+  HLSUnsupportedError,
+  VideoEmbedInnerWeb,
+} from 'view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb'
 import {atoms as a, useTheme} from '#/alf'
 import {Button, ButtonText} from '#/components/Button'
 import {Text} from '#/components/Typography'
 import {ErrorBoundary} from '../ErrorBoundary'
 import {useActiveVideoView} from './ActiveVideoContext'
-import {VideoEmbedInner} from './VideoEmbedInner'
-import {HLSUnsupportedError} from './VideoEmbedInner.web'
 
 export function VideoEmbed({source}: {source: string}) {
   const t = useTheme()
@@ -60,7 +62,7 @@ export function VideoEmbed({source}: {source: string}) {
           <ViewportObserver
             sendPosition={sendPosition}
             isAnyViewActive={currentActiveView !== null}>
-            <VideoEmbedInner
+            <VideoEmbedInnerWeb
               source={source}
               active={active}
               setActive={setActive}
diff --git a/src/view/com/util/post-embeds/VideoEmbedInner.tsx b/src/view/com/util/post-embeds/VideoEmbedInner.tsx
deleted file mode 100644
index 9b1fd54fb..000000000
--- a/src/view/com/util/post-embeds/VideoEmbedInner.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import React, {useCallback, useEffect, useRef, useState} from 'react'
-import {Pressable, StyleSheet, useWindowDimensions, View} from 'react-native'
-import Animated, {
-  measure,
-  runOnJS,
-  useAnimatedRef,
-  useFrameCallback,
-  useSharedValue,
-} from 'react-native-reanimated'
-import {VideoPlayer, VideoView} from 'expo-video'
-
-import {atoms as a} from '#/alf'
-import {Text} from '#/components/Typography'
-import {useVideoPlayer} from './VideoPlayerContext'
-
-export function VideoEmbedInner({}: {
-  source: string
-  active: boolean
-  setActive: () => void
-  onScreen: boolean
-}) {
-  const player = useVideoPlayer()
-  const aref = useAnimatedRef<Animated.View>()
-  const {height: windowHeight} = useWindowDimensions()
-  const hasLeftView = useSharedValue(false)
-  const ref = useRef<VideoView>(null)
-
-  const onEnterView = useCallback(() => {
-    if (player.status === 'readyToPlay') {
-      player.play()
-    }
-  }, [player])
-
-  const onLeaveView = useCallback(() => {
-    player.pause()
-  }, [player])
-
-  const enterFullscreen = useCallback(() => {
-    if (ref.current) {
-      ref.current.enterFullscreen()
-    }
-  }, [])
-
-  useFrameCallback(() => {
-    const measurement = measure(aref)
-
-    if (measurement) {
-      if (hasLeftView.value) {
-        // Check if the video is in view
-        if (
-          measurement.pageY >= 0 &&
-          measurement.pageY + measurement.height <= windowHeight
-        ) {
-          runOnJS(onEnterView)()
-          hasLeftView.value = false
-        }
-      } else {
-        // Check if the video is out of view
-        if (
-          measurement.pageY + measurement.height < 0 ||
-          measurement.pageY > windowHeight
-        ) {
-          runOnJS(onLeaveView)()
-          hasLeftView.value = true
-        }
-      }
-    }
-  })
-
-  return (
-    <Animated.View
-      style={[a.flex_1, a.relative]}
-      ref={aref}
-      collapsable={false}>
-      <VideoView
-        ref={ref}
-        player={player}
-        style={a.flex_1}
-        nativeControls={true}
-      />
-      <VideoControls player={player} enterFullscreen={enterFullscreen} />
-    </Animated.View>
-  )
-}
-
-function VideoControls({
-  player,
-  enterFullscreen,
-}: {
-  player: VideoPlayer
-  enterFullscreen: () => void
-}) {
-  const [currentTime, setCurrentTime] = useState(Math.floor(player.currentTime))
-
-  useEffect(() => {
-    const interval = setInterval(() => {
-      setCurrentTime(Math.floor(player.duration - player.currentTime))
-      // how often should we update the time?
-      // 1000 gets out of sync with the video time
-    }, 250)
-
-    return () => {
-      clearInterval(interval)
-    }
-  }, [player])
-
-  const minutes = Math.floor(currentTime / 60)
-  const seconds = String(currentTime % 60).padStart(2, '0')
-
-  return (
-    <View style={[a.absolute, a.inset_0]}>
-      <View style={styles.timeContainer} pointerEvents="none">
-        <Text style={styles.timeElapsed}>
-          {minutes}:{seconds}
-        </Text>
-      </View>
-      <Pressable
-        onPress={enterFullscreen}
-        style={a.flex_1}
-        accessibilityLabel="Video"
-        accessibilityHint="Tap to enter full screen"
-        accessibilityRole="button"
-      />
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  timeContainer: {
-    backgroundColor: 'rgba(0, 0, 0, 0.75)',
-    borderRadius: 6,
-    paddingHorizontal: 6,
-    paddingVertical: 3,
-    position: 'absolute',
-    left: 5,
-    bottom: 5,
-  },
-  timeElapsed: {
-    color: 'white',
-    fontSize: 12,
-    fontWeight: 'bold',
-  },
-})
diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx
new file mode 100644
index 000000000..cc356fb06
--- /dev/null
+++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx
@@ -0,0 +1,96 @@
+import React, {useEffect, useRef, useState} from 'react'
+import {Pressable, View} from 'react-native'
+import {VideoPlayer, VideoView} from 'expo-video'
+
+import {useVideoPlayer} from 'view/com/util/post-embeds/VideoPlayerContext'
+import {android, atoms as a} from '#/alf'
+import {Text} from '#/components/Typography'
+
+export function VideoEmbedInnerNative() {
+  const player = useVideoPlayer()
+  const ref = useRef<VideoView>(null)
+
+  return (
+    <View style={[a.flex_1, a.relative]} collapsable={false}>
+      <VideoView
+        ref={ref}
+        player={player}
+        style={a.flex_1}
+        nativeControls={true}
+      />
+      <Controls
+        player={player}
+        enterFullscreen={() => ref.current?.enterFullscreen()}
+      />
+    </View>
+  )
+}
+
+function Controls({
+  player,
+  enterFullscreen,
+}: {
+  player: VideoPlayer
+  enterFullscreen: () => void
+}) {
+  const [duration, setDuration] = useState(() => Math.floor(player.duration))
+  const [currentTime, setCurrentTime] = useState(() =>
+    Math.floor(player.currentTime),
+  )
+
+  const timeRemaining = duration - currentTime
+  const minutes = Math.floor(timeRemaining / 60)
+  const seconds = String(timeRemaining % 60).padStart(2, '0')
+
+  useEffect(() => {
+    const interval = setInterval(() => {
+      // duration gets reset to 0 on loop
+      if (player.duration) setDuration(Math.floor(player.duration))
+      setCurrentTime(Math.floor(player.currentTime))
+      // how often should we update the time?
+      // 1000 gets out of sync with the video time
+    }, 250)
+
+    return () => {
+      clearInterval(interval)
+    }
+  }, [player])
+
+  if (isNaN(timeRemaining)) {
+    return null
+  }
+
+  return (
+    <View style={[a.absolute, a.inset_0]}>
+      <View
+        style={[
+          {
+            backgroundColor: 'rgba(0, 0, 0, 0.75',
+            borderRadius: 6,
+            paddingHorizontal: 6,
+            paddingVertical: 3,
+            position: 'absolute',
+            left: 5,
+            bottom: 5,
+          },
+        ]}
+        pointerEvents="none">
+        <Text
+          style={[
+            {color: 'white', fontSize: 12},
+            a.font_bold,
+            android({lineHeight: 1.25}),
+          ]}>
+          {minutes}:{seconds}
+        </Text>
+      </View>
+      <Pressable
+        onPress={enterFullscreen}
+        style={a.flex_1}
+        accessibilityLabel="Video"
+        accessibilityHint="Tap to enter full screen"
+        accessibilityRole="button"
+      />
+    </View>
+  )
+}
diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.web.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.web.tsx
new file mode 100644
index 000000000..59da5be42
--- /dev/null
+++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.web.tsx
@@ -0,0 +1,3 @@
+export function VideoEmbedInnerNative() {
+  throw new Error('VideoEmbedInnerNative may not be used on native.')
+}
diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.native.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.native.tsx
new file mode 100644
index 000000000..8664aae14
--- /dev/null
+++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.native.tsx
@@ -0,0 +1,3 @@
+export function VideoEmbedInnerWeb() {
+  throw new Error('VideoEmbedInnerWeb may not be used on native.')
+}
diff --git a/src/view/com/util/post-embeds/VideoEmbedInner.web.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx
index f5f47db50..c0021d9bb 100644
--- a/src/view/com/util/post-embeds/VideoEmbedInner.web.tsx
+++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx
@@ -5,17 +5,23 @@ import Hls from 'hls.js'
 import {atoms as a} from '#/alf'
 import {Controls} from './VideoWebControls'
 
-export function VideoEmbedInner({
+export function VideoEmbedInnerWeb({
   source,
   active,
   setActive,
   onScreen,
 }: {
   source: string
-  active: boolean
-  setActive: () => void
-  onScreen: boolean
+  active?: boolean
+  setActive?: () => void
+  onScreen?: boolean
 }) {
+  if (active == null || setActive == null || onScreen == null) {
+    throw new Error(
+      'active, setActive, and onScreen are required VideoEmbedInner props on web.',
+    )
+  }
+
   const containerRef = useRef<HTMLDivElement>(null)
   const ref = useRef<HTMLVideoElement>(null)
   const [focused, setFocused] = useState(false)
diff --git a/src/view/com/util/post-embeds/VideoWebControls.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx
index 11e0867e4..11e0867e4 100644
--- a/src/view/com/util/post-embeds/VideoWebControls.tsx
+++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx
diff --git a/src/view/com/util/post-embeds/VideoWebControls.web.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.web.tsx
index 2843664be..7caaf3abf 100644
--- a/src/view/com/util/post-embeds/VideoWebControls.web.tsx
+++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.web.tsx
@@ -11,12 +11,12 @@ import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import type Hls from 'hls.js'
 
-import {isIPhoneWeb} from '#/platform/detection'
+import {isIPhoneWeb} from 'platform/detection'
 import {
   useAutoplayDisabled,
   useSetSubtitlesEnabled,
   useSubtitlesEnabled,
-} from '#/state/preferences'
+} from 'state/preferences'
 import {atoms as a, useTheme, web} from '#/alf'
 import {Button} from '#/components/Button'
 import {useInteractionState} from '#/components/hooks/useInteractionState'