about summary refs log tree commit diff
path: root/src/components/Post/Embed/VideoEmbed/index.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/index.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/index.tsx')
-rw-r--r--src/components/Post/Embed/VideoEmbed/index.tsx167
1 files changed, 167 insertions, 0 deletions
diff --git a/src/components/Post/Embed/VideoEmbed/index.tsx b/src/components/Post/Embed/VideoEmbed/index.tsx
new file mode 100644
index 000000000..fe29ecad6
--- /dev/null
+++ b/src/components/Post/Embed/VideoEmbed/index.tsx
@@ -0,0 +1,167 @@
+import React, {useCallback, useState} from 'react'
+import {ActivityIndicator, View} from 'react-native'
+import {ImageBackground} from 'expo-image'
+import {AppBskyEmbedVideo} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
+import {ConstrainedImage} from '#/view/com/util/images/AutoSizedImage'
+import {atoms as a, useTheme} from '#/alf'
+import {Button} from '#/components/Button'
+import {useThrottledValue} from '#/components/hooks/useThrottledValue'
+import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
+import {VideoEmbedInnerNative} from './VideoEmbedInner/VideoEmbedInnerNative'
+import * as VideoFallback from './VideoEmbedInner/VideoFallback'
+
+interface Props {
+  embed: AppBskyEmbedVideo.View
+  crop?: 'none' | 'square' | 'constrained'
+}
+
+export function VideoEmbed({embed, crop}: Props) {
+  const t = useTheme()
+  const [key, setKey] = useState(0)
+
+  const renderError = useCallback(
+    (error: unknown) => (
+      <VideoError error={error} retry={() => setKey(key + 1)} />
+    ),
+    [key],
+  )
+
+  let aspectRatio: number | undefined
+  const dims = embed.aspectRatio
+  if (dims) {
+    aspectRatio = dims.width / dims.height
+    if (Number.isNaN(aspectRatio)) {
+      aspectRatio = undefined
+    }
+  }
+
+  let constrained: number | undefined
+  let max: number | undefined
+  if (aspectRatio !== undefined) {
+    const ratio = 1 / 2 // max of 1:2 ratio in feeds
+    constrained = Math.max(aspectRatio, ratio)
+    max = Math.max(aspectRatio, 0.25) // max of 1:4 in thread
+  }
+  const cropDisabled = crop === 'none'
+
+  const contents = (
+    <ErrorBoundary renderError={renderError} key={key}>
+      <InnerWrapper embed={embed} />
+    </ErrorBoundary>
+  )
+
+  return (
+    <View style={[a.pt_xs]}>
+      {cropDisabled ? (
+        <View
+          style={[
+            a.w_full,
+            a.overflow_hidden,
+            {aspectRatio: max ?? 1},
+            a.rounded_md,
+            a.overflow_hidden,
+            t.atoms.bg_contrast_25,
+          ]}>
+          {contents}
+        </View>
+      ) : (
+        <ConstrainedImage
+          fullBleed={crop === 'square'}
+          aspectRatio={constrained || 1}>
+          {contents}
+        </ConstrainedImage>
+      )}
+    </View>
+  )
+}
+
+function InnerWrapper({embed}: Props) {
+  const {_} = useLingui()
+  const ref = React.useRef<{togglePlayback: () => void}>(null)
+
+  const [status, setStatus] = React.useState<'playing' | 'paused' | 'pending'>(
+    'pending',
+  )
+  const [isLoading, setIsLoading] = React.useState(false)
+  const [isActive, setIsActive] = React.useState(false)
+  const showSpinner = useThrottledValue(isActive && isLoading, 100)
+
+  const showOverlay =
+    !isActive ||
+    isLoading ||
+    (status === 'paused' && !isActive) ||
+    status === 'pending'
+
+  React.useEffect(() => {
+    if (!isActive && status !== 'pending') {
+      setStatus('pending')
+    }
+  }, [isActive, status])
+
+  return (
+    <>
+      <VideoEmbedInnerNative
+        embed={embed}
+        setStatus={setStatus}
+        setIsLoading={setIsLoading}
+        setIsActive={setIsActive}
+        ref={ref}
+      />
+      <ImageBackground
+        source={{uri: embed.thumbnail}}
+        accessibilityIgnoresInvertColors
+        style={[
+          a.absolute,
+          a.inset_0,
+          {
+            backgroundColor: 'transparent', // If you don't add `backgroundColor` to the styles here,
+            // the play button won't show up on the first render on android 🥴😮‍💨
+            display: showOverlay ? 'flex' : 'none',
+          },
+        ]}
+        cachePolicy="memory-disk" // Preferring memory cache helps to avoid flicker when re-displaying on android
+      >
+        {showOverlay && (
+          <Button
+            style={[a.flex_1, a.align_center, a.justify_center]}
+            onPress={() => {
+              ref.current?.togglePlayback()
+            }}
+            label={_(msg`Play video`)}
+            color="secondary">
+            {showSpinner ? (
+              <View
+                style={[
+                  a.rounded_full,
+                  a.p_xs,
+                  a.align_center,
+                  a.justify_center,
+                ]}>
+                <ActivityIndicator size="large" color="white" />
+              </View>
+            ) : (
+              <PlayButtonIcon />
+            )}
+          </Button>
+        )}
+      </ImageBackground>
+    </>
+  )
+}
+
+function VideoError({retry}: {error: unknown; retry: () => void}) {
+  return (
+    <VideoFallback.Container>
+      <VideoFallback.Text>
+        <Trans>
+          An error occurred while loading the video. Please try again later.
+        </Trans>
+      </VideoFallback.Text>
+      <VideoFallback.RetryButton onPress={retry} />
+    </VideoFallback.Container>
+  )
+}