about summary refs log tree commit diff
path: root/src/view
diff options
context:
space:
mode:
Diffstat (limited to 'src/view')
-rw-r--r--src/view/com/util/post-embeds/ExternalLinkEmbed.tsx18
-rw-r--r--src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx251
-rw-r--r--src/view/com/util/post-embeds/YoutubeEmbed.tsx55
-rw-r--r--src/view/com/util/post-embeds/index.tsx18
4 files changed, 269 insertions, 73 deletions
diff --git a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx
index d5bb38fb2..27aa804d3 100644
--- a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx
+++ b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx
@@ -6,22 +6,28 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {AppBskyEmbedExternal} from '@atproto/api'
 import {toNiceDomain} from 'lib/strings/url-helpers'
+import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player'
+import {ExternalPlayer} from 'view/com/util/post-embeds/ExternalPlayerEmbed'
 
 export const ExternalLinkEmbed = ({
   link,
-  imageChild,
 }: {
   link: AppBskyEmbedExternal.ViewExternal
-  imageChild?: React.ReactNode
 }) => {
   const pal = usePalette('default')
   const {isMobile} = useWebMediaQueries()
+
+  const embedPlayerParams = React.useMemo(
+    () => parseEmbedPlayerFromUrl(link.uri),
+    [link.uri],
+  )
+
   return (
     <View
       style={{
-        flexDirection: isMobile ? 'column' : 'row',
+        flexDirection: !isMobile && !embedPlayerParams ? 'row' : 'column',
       }}>
-      {link.thumb ? (
+      {link.thumb && !embedPlayerParams ? (
         <View
           style={
             !isMobile
@@ -45,9 +51,11 @@ export const ExternalLinkEmbed = ({
             source={{uri: link.thumb}}
             accessibilityIgnoresInvertColors
           />
-          {imageChild}
         </View>
       ) : undefined}
+      {embedPlayerParams && (
+        <ExternalPlayer link={link} params={embedPlayerParams} />
+      )}
       <View
         style={{
           paddingHorizontal: isMobile ? 10 : 14,
diff --git a/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx b/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx
new file mode 100644
index 000000000..580cf363a
--- /dev/null
+++ b/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx
@@ -0,0 +1,251 @@
+import React from 'react'
+import {
+  ActivityIndicator,
+  Dimensions,
+  GestureResponderEvent,
+  Pressable,
+  StyleSheet,
+  View,
+} from 'react-native'
+import {Image} from 'expo-image'
+import {WebView} from 'react-native-webview'
+import YoutubePlayer from 'react-native-youtube-iframe'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {EmbedPlayerParams, getPlayerHeight} from 'lib/strings/embed-player'
+import {EventStopper} from '../EventStopper'
+import {AppBskyEmbedExternal} from '@atproto/api'
+import {isNative} from 'platform/detection'
+import {useNavigation} from '@react-navigation/native'
+import {NavigationProp} from 'lib/routes/types'
+
+interface ShouldStartLoadRequest {
+  url: string
+}
+
+// This renders the overlay when the player is either inactive or loading as a separate layer
+function PlaceholderOverlay({
+  isLoading,
+  isPlayerActive,
+  onPress,
+}: {
+  isLoading: boolean
+  isPlayerActive: boolean
+  onPress: (event: GestureResponderEvent) => void
+}) {
+  // If the player is active and not loading, we don't want to show the overlay.
+  if (isPlayerActive && !isLoading) return null
+
+  return (
+    <View style={[styles.layer, styles.overlayLayer]}>
+      <Pressable
+        accessibilityRole="button"
+        accessibilityLabel="Play Video"
+        accessibilityHint=""
+        onPress={onPress}
+        style={[styles.overlayContainer, styles.topRadius]}>
+        {!isPlayerActive ? (
+          <FontAwesomeIcon icon="play" size={42} color="white" />
+        ) : (
+          <ActivityIndicator size="large" color="white" />
+        )}
+      </Pressable>
+    </View>
+  )
+}
+
+// This renders the webview/youtube player as a separate layer
+function Player({
+  height,
+  params,
+  onLoad,
+  isPlayerActive,
+}: {
+  isPlayerActive: boolean
+  params: EmbedPlayerParams
+  height: number
+  onLoad: () => void
+}) {
+  // ensures we only load what's requested
+  const onShouldStartLoadWithRequest = React.useCallback(
+    (event: ShouldStartLoadRequest) => event.url === params.playerUri,
+    [params.playerUri],
+  )
+
+  // Don't show the player until it is active
+  if (!isPlayerActive) return null
+
+  return (
+    <View style={[styles.layer, styles.playerLayer]}>
+      <EventStopper>
+        {isNative && params.type === 'youtube_video' ? (
+          <YoutubePlayer
+            videoId={params.videoId}
+            play
+            height={height}
+            onReady={onLoad}
+            webViewStyle={[styles.webview, styles.topRadius]}
+          />
+        ) : (
+          <View style={{height, width: '100%'}}>
+            <WebView
+              javaScriptEnabled={true}
+              onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
+              mediaPlaybackRequiresUserAction={false}
+              allowsInlineMediaPlayback
+              bounces={false}
+              allowsFullscreenVideo
+              nestedScrollEnabled
+              source={{uri: params.playerUri}}
+              onLoad={onLoad}
+              setSupportMultipleWindows={false} // Prevent any redirects from opening a new window (ads)
+              style={[styles.webview, styles.topRadius]}
+            />
+          </View>
+        )}
+      </EventStopper>
+    </View>
+  )
+}
+
+// This renders the player area and handles the logic for when to show the player and when to show the overlay
+export function ExternalPlayer({
+  link,
+  params,
+}: {
+  link: AppBskyEmbedExternal.ViewExternal
+  params: EmbedPlayerParams
+}) {
+  const navigation = useNavigation<NavigationProp>()
+
+  const [isPlayerActive, setPlayerActive] = React.useState(false)
+  const [isLoading, setIsLoading] = React.useState(true)
+  const [dim, setDim] = React.useState({
+    width: 0,
+    height: 0,
+  })
+
+  const viewRef = React.useRef<View>(null)
+
+  // watch for leaving the viewport due to scrolling
+  React.useEffect(() => {
+    // Interval for scrolling works in most cases, However, for twitch embeds, if we navigate away from the screen the webview will
+    // continue playing. We need to watch for the blur event
+    const unsubscribe = navigation.addListener('blur', () => {
+      setPlayerActive(false)
+    })
+
+    const interval = setInterval(() => {
+      viewRef.current?.measure((x, y, w, h, pageX, pageY) => {
+        const window = Dimensions.get('window')
+        const top = pageY
+        const bot = pageY + h
+        const isVisible = isNative
+          ? top >= 0 && bot <= window.height
+          : !(top >= window.height || bot <= 0)
+        if (!isVisible) {
+          setPlayerActive(false)
+        }
+      })
+    }, 1e3)
+    return () => {
+      unsubscribe()
+      clearInterval(interval)
+    }
+  }, [viewRef, navigation])
+
+  // calculate height for the player and the screen size
+  const height = React.useMemo(
+    () =>
+      getPlayerHeight({
+        type: params.type,
+        width: dim.width,
+        hasThumb: !!link.thumb,
+      }),
+    [params.type, dim.width, link.thumb],
+  )
+
+  const onLoad = React.useCallback(() => {
+    setIsLoading(false)
+  }, [])
+
+  const onPlayPress = React.useCallback((event: GestureResponderEvent) => {
+    // Prevent this from propagating upward on web
+    event.preventDefault()
+
+    setPlayerActive(true)
+  }, [])
+
+  // measure the layout to set sizing
+  const onLayout = React.useCallback(
+    (event: {nativeEvent: {layout: {width: any; height: any}}}) => {
+      setDim({
+        width: event.nativeEvent.layout.width,
+        height: event.nativeEvent.layout.height,
+      })
+    },
+    [],
+  )
+
+  return (
+    <View
+      ref={viewRef}
+      style={{height}}
+      collapsable={false}
+      onLayout={onLayout}>
+      {link.thumb && (!isPlayerActive || isLoading) && (
+        <Image
+          style={[
+            {
+              width: dim.width,
+              height,
+            },
+            styles.topRadius,
+          ]}
+          source={{uri: link.thumb}}
+          accessibilityIgnoresInvertColors
+        />
+      )}
+
+      <PlaceholderOverlay
+        isLoading={isLoading}
+        isPlayerActive={isPlayerActive}
+        onPress={onPlayPress}
+      />
+      <Player
+        isPlayerActive={isPlayerActive}
+        params={params}
+        height={height}
+        onLoad={onLoad}
+      />
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  topRadius: {
+    borderTopLeftRadius: 6,
+    borderTopRightRadius: 6,
+  },
+  layer: {
+    position: 'absolute',
+    top: 0,
+    left: 0,
+    right: 0,
+    bottom: 0,
+  },
+  overlayContainer: {
+    flex: 1,
+    justifyContent: 'center',
+    alignItems: 'center',
+    backgroundColor: 'rgba(0,0,0,0.5)',
+  },
+  overlayLayer: {
+    zIndex: 2,
+  },
+  playerLayer: {
+    zIndex: 3,
+  },
+  webview: {
+    backgroundColor: 'transparent',
+  },
+})
diff --git a/src/view/com/util/post-embeds/YoutubeEmbed.tsx b/src/view/com/util/post-embeds/YoutubeEmbed.tsx
deleted file mode 100644
index 2f2da5662..000000000
--- a/src/view/com/util/post-embeds/YoutubeEmbed.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import React from 'react'
-import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
-import {usePalette} from 'lib/hooks/usePalette'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {ExternalLinkEmbed} from './ExternalLinkEmbed'
-import {AppBskyEmbedExternal} from '@atproto/api'
-import {Link} from '../Link'
-
-export const YoutubeEmbed = ({
-  link,
-  style,
-}: {
-  link: AppBskyEmbedExternal.ViewExternal
-  style?: StyleProp<ViewStyle>
-}) => {
-  const pal = usePalette('default')
-
-  const imageChild = (
-    <View style={styles.playButton}>
-      <FontAwesomeIcon icon="play" size={24} color="white" />
-    </View>
-  )
-
-  return (
-    <Link
-      asAnchor
-      style={[styles.extOuter, pal.view, pal.border, style]}
-      href={link.uri}>
-      <ExternalLinkEmbed link={link} imageChild={imageChild} />
-    </Link>
-  )
-}
-
-const styles = StyleSheet.create({
-  extOuter: {
-    borderWidth: 1,
-    borderRadius: 8,
-  },
-  playButton: {
-    position: 'absolute',
-    alignSelf: 'center',
-    alignItems: 'center',
-    top: '44%',
-    justifyContent: 'center',
-    backgroundColor: 'black',
-    padding: 10,
-    borderRadius: 50,
-    opacity: 0.8,
-  },
-  webView: {
-    alignItems: 'center',
-    alignContent: 'center',
-    justifyContent: 'center',
-  },
-})
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index 2814cad87..c94ce9684 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -23,9 +23,7 @@ import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
 import {useLightboxControls, ImagesLightbox} from '#/state/lightbox'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {YoutubeEmbed} from './YoutubeEmbed'
 import {ExternalLinkEmbed} from './ExternalLinkEmbed'
-import {getYoutubeVideoId} from 'lib/strings/url-helpers'
 import {MaybeQuoteEmbed} from './QuoteEmbed'
 import {AutoSizedImage} from '../images/AutoSizedImage'
 import {ListEmbed} from './ListEmbed'
@@ -168,19 +166,13 @@ export function PostEmbeds({
   // =
   if (AppBskyEmbedExternal.isView(embed)) {
     const link = embed.external
-    const youtubeVideoId = getYoutubeVideoId(link.uri)
-
-    if (youtubeVideoId) {
-      return <YoutubeEmbed link={link} style={style} />
-    }
 
     return (
-      <Link
-        asAnchor
-        style={[styles.extOuter, pal.view, pal.border, style]}
-        href={link.uri}>
-        <ExternalLinkEmbed link={link} />
-      </Link>
+      <View style={[styles.extOuter, pal.view, pal.border, style]}>
+        <Link asAnchor href={link.uri}>
+          <ExternalLinkEmbed link={link} />
+        </Link>
+      </View>
     )
   }