diff options
Diffstat (limited to 'src/view/com/util/PostEmbeds')
-rw-r--r-- | src/view/com/util/PostEmbeds/ExternalLinkEmbed.tsx | 69 | ||||
-rw-r--r-- | src/view/com/util/PostEmbeds/YoutubeEmbed.tsx | 119 | ||||
-rw-r--r-- | src/view/com/util/PostEmbeds/index.tsx | 139 |
3 files changed, 327 insertions, 0 deletions
diff --git a/src/view/com/util/PostEmbeds/ExternalLinkEmbed.tsx b/src/view/com/util/PostEmbeds/ExternalLinkEmbed.tsx new file mode 100644 index 000000000..e8c63bdb7 --- /dev/null +++ b/src/view/com/util/PostEmbeds/ExternalLinkEmbed.tsx @@ -0,0 +1,69 @@ +import React from 'react' +import {Text} from '../text/Text' +import {AutoSizedImage} from '../images/AutoSizedImage' +import {StyleSheet, View} from 'react-native' +import {usePalette} from 'lib/hooks/usePalette' +import {PresentedExternal} from '@atproto/api/dist/client/types/app/bsky/embed/external' + +const ExternalLinkEmbed = ({ + link, + onImagePress, + imageChild, +}: { + link: PresentedExternal + onImagePress?: () => void + imageChild?: React.ReactNode +}) => { + const pal = usePalette('default') + return ( + <> + {link.thumb ? ( + <AutoSizedImage + uri={link.thumb} + style={styles.extImage} + onPress={onImagePress}> + {imageChild} + </AutoSizedImage> + ) : undefined} + <View style={styles.extInner}> + <Text type="md-bold" numberOfLines={2} style={[pal.text]}> + {link.title || link.uri} + </Text> + <Text + type="sm" + numberOfLines={1} + style={[pal.textLight, styles.extUri]}> + {link.uri} + </Text> + {link.description ? ( + <Text + type="sm" + numberOfLines={2} + style={[pal.text, styles.extDescription]}> + {link.description} + </Text> + ) : undefined} + </View> + </> + ) +} + +const styles = StyleSheet.create({ + extInner: { + padding: 10, + }, + extImage: { + borderTopLeftRadius: 6, + borderTopRightRadius: 6, + width: '100%', + maxHeight: 200, + }, + extUri: { + marginTop: 2, + }, + extDescription: { + marginTop: 4, + }, +}) + +export default ExternalLinkEmbed diff --git a/src/view/com/util/PostEmbeds/YoutubeEmbed.tsx b/src/view/com/util/PostEmbeds/YoutubeEmbed.tsx new file mode 100644 index 000000000..d9425fe4e --- /dev/null +++ b/src/view/com/util/PostEmbeds/YoutubeEmbed.tsx @@ -0,0 +1,119 @@ +import React, {useEffect} from 'react' +import {useState} from 'react' +import { + View, + StyleSheet, + Pressable, + TouchableWithoutFeedback, + EmitterSubscription, +} from 'react-native' +import YoutubePlayer from 'react-native-youtube-iframe' +import {usePalette} from 'lib/hooks/usePalette' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import ExternalLinkEmbed from './ExternalLinkEmbed' +import {PresentedExternal} from '@atproto/api/dist/client/types/app/bsky/embed/external' +import {useStores} from 'state/index' + +const YoutubeEmbed = ({ + link, + videoId, +}: { + videoId: string + link: PresentedExternal +}) => { + const store = useStores() + const [displayVideoPlayer, setDisplayVideoPlayer] = useState(false) + const [playerDimensions, setPlayerDimensions] = useState({ + width: 0, + height: 0, + }) + const pal = usePalette('default') + const handlePlayButtonPressed = () => { + setDisplayVideoPlayer(true) + } + const handleOnLayout = (event: { + nativeEvent: {layout: {width: any; height: any}} + }) => { + setPlayerDimensions({ + width: event.nativeEvent.layout.width, + height: event.nativeEvent.layout.height, + }) + } + useEffect(() => { + let sub: EmitterSubscription + if (displayVideoPlayer) { + sub = store.onNavigation(() => { + setDisplayVideoPlayer(false) + }) + } + return () => sub && sub.remove() + }, [displayVideoPlayer, store]) + + const imageChild = ( + <Pressable onPress={handlePlayButtonPressed} style={styles.playButton}> + <FontAwesomeIcon icon="play" size={24} color="white" /> + </Pressable> + ) + + if (!displayVideoPlayer) { + return ( + <View + style={[styles.extOuter, pal.view, pal.border]} + onLayout={handleOnLayout}> + <ExternalLinkEmbed + link={link} + onImagePress={handlePlayButtonPressed} + imageChild={imageChild} + /> + </View> + ) + } + + const height = (playerDimensions.width / 16) * 9 + const noop = () => {} + + return ( + <TouchableWithoutFeedback onPress={noop}> + <View> + {/* Removing the outter View will make tap events propagate to parents */} + <YoutubePlayer + initialPlayerParams={{ + modestbranding: true, + }} + webViewProps={{ + startInLoadingState: true, + }} + height={height} + videoId={videoId} + webViewStyle={styles.webView} + /> + </View> + </TouchableWithoutFeedback> + ) +} + +const styles = StyleSheet.create({ + extOuter: { + borderWidth: 1, + borderRadius: 8, + marginTop: 4, + }, + 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', + }, +}) + +export default YoutubeEmbed diff --git a/src/view/com/util/PostEmbeds/index.tsx b/src/view/com/util/PostEmbeds/index.tsx new file mode 100644 index 000000000..031f01e88 --- /dev/null +++ b/src/view/com/util/PostEmbeds/index.tsx @@ -0,0 +1,139 @@ +import React from 'react' +import { + StyleSheet, + StyleProp, + View, + ViewStyle, + Image as RNImage, +} from 'react-native' +import {AppBskyEmbedImages, AppBskyEmbedExternal} from '@atproto/api' +import {Link} from '../Link' +import {AutoSizedImage} from '../images/AutoSizedImage' +import {ImageLayoutGrid} from '../images/ImageLayoutGrid' +import {ImagesLightbox} from 'state/models/shell-ui' +import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' +import {saveImageModal} from 'lib/images' +import YoutubeEmbed from './YoutubeEmbed' +import ExternalLinkEmbed from './ExternalLinkEmbed' +import {getYoutubeVideoId} from 'lib/strings/url-helpers' + +type Embed = + | AppBskyEmbedImages.Presented + | AppBskyEmbedExternal.Presented + | {$type: string; [k: string]: unknown} + +export function PostEmbeds({ + embed, + style, +}: { + embed?: Embed + style?: StyleProp<ViewStyle> +}) { + const pal = usePalette('default') + const store = useStores() + if (AppBskyEmbedImages.isPresented(embed)) { + if (embed.images.length > 0) { + const uris = embed.images.map(img => img.fullsize) + const openLightbox = (index: number) => { + store.shell.openLightbox(new ImagesLightbox(uris, index)) + } + const onLongPress = (index: number) => { + saveImageModal({uri: uris[index]}) + } + const onPressIn = (index: number) => { + const firstImageToShow = uris[index] + RNImage.prefetch(firstImageToShow) + uris.forEach(uri => { + if (firstImageToShow !== uri) { + // First image already prefeched above + RNImage.prefetch(uri) + } + }) + } + + if (embed.images.length === 4) { + return ( + <View style={[styles.imagesContainer, style]}> + <ImageLayoutGrid + type="four" + uris={embed.images.map(img => img.thumb)} + onPress={openLightbox} + onLongPress={onLongPress} + onPressIn={onPressIn} + /> + </View> + ) + } else if (embed.images.length === 3) { + return ( + <View style={[styles.imagesContainer, style]}> + <ImageLayoutGrid + type="three" + uris={embed.images.map(img => img.thumb)} + onPress={openLightbox} + onLongPress={onLongPress} + onPressIn={onPressIn} + /> + </View> + ) + } else if (embed.images.length === 2) { + return ( + <View style={[styles.imagesContainer, style]}> + <ImageLayoutGrid + type="two" + uris={embed.images.map(img => img.thumb)} + onPress={openLightbox} + onLongPress={onLongPress} + onPressIn={onPressIn} + /> + </View> + ) + } else { + return ( + <View style={[styles.imagesContainer, style]}> + <AutoSizedImage + uri={embed.images[0].thumb} + onPress={() => openLightbox(0)} + onLongPress={() => onLongPress(0)} + onPressIn={() => onPressIn(0)} + style={styles.singleImage} + /> + </View> + ) + } + } + } + if (AppBskyEmbedExternal.isPresented(embed)) { + const link = embed.external + const youtubeVideoId = getYoutubeVideoId(link.uri) + + if (youtubeVideoId) { + return <YoutubeEmbed videoId={youtubeVideoId} link={link} /> + } + + return ( + <Link + style={[styles.extOuter, pal.view, pal.border, style]} + href={link.uri} + noFeedback> + <ExternalLinkEmbed link={link} /> + </Link> + ) + } + return <View /> +} + +const styles = StyleSheet.create({ + imagesContainer: { + marginTop: 4, + }, + singleImage: { + borderRadius: 8, + maxHeight: 500, + }, + extOuter: { + borderWidth: 1, + borderRadius: 8, + marginTop: 4, + }, +}) |