diff options
Diffstat (limited to 'src/view/com')
-rw-r--r-- | src/view/com/util/post-embeds/ExternalLinkEmbed.tsx | 18 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx | 251 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/YoutubeEmbed.tsx | 55 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/index.tsx | 18 |
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> ) } |