From fedb94dd70903ba5b653bd7fc76800ddb1f2bc4d Mon Sep 17 00:00:00 2001 From: Hailey <153161762+haileyok@users.noreply.github.com> Date: Thu, 21 Dec 2023 14:33:46 -0800 Subject: 3rd party embed player (#2217) * Implement embed player for YT, spotify, and twitch * fix: handle blur event * fix: use video dimensions for twitch * fix: remove hack (?) * fix: remove origin whitelist (?) * fix: prevent ads from opening in browser * fix: handle embeds that don't have a thumb * feat: handle dark/light mode * fix: ts warning * fix: adjust height of no-thumb label * fix: adjust height of no-thumb label * fix: remove debug log, set collapsable to false for player view * fix: fix dimensions "flash" * chore: remove old youtube link test * tests: add tests * fix: thumbless embed position when loading * fix: remove background from webview * cleanup embeds (almost) * more refactoring - Use separate layers for player and overlay to prevent weird sizing issues - Be sure the image is not visible under the player - Clean up some * cleanup styles * parse youtube shorts urls * remove debug * add soundcloud tracks and sets (playlists) * move logic into `ExternalLinkEmbed` * border radius for yt player on native * fix styling on web * allow scrolling in webview on android * remove unnecessary check * autoplay yt on web * fix tests after adding autoplay * move `useNavigation` to top of component --------- Co-authored-by: Paul Frazee --- .../com/util/post-embeds/ExternalPlayerEmbed.tsx | 251 +++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx (limited to 'src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx') 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 ( + + + {!isPlayerActive ? ( + + ) : ( + + )} + + + ) +} + +// 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 ( + + + {isNative && params.type === 'youtube_video' ? ( + + ) : ( + + + + )} + + + ) +} + +// 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() + + 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(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 ( + + {link.thumb && (!isPlayerActive || isLoading) && ( + + )} + + + + + ) +} + +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', + }, +}) -- cgit 1.4.1