* add apple music embed * add vimeo embed * add logic for tenor and giphy embeds * keep it simple, use playerUri for images too * add gif embed player * lint, fix tests * remove links that can't produce a thumb * Revert "remove links that can't produce a thumb" This reverts commit 985b92b4e622db936bb0c79fdf324099b9c8fcd8. * Revert "Revert "remove links that can't produce a thumb"" This reverts commit 4895ded8b5120c4fc52b43ae85c9a01ea0b1a733. * Revert "Revert "Revert "remove links that can't produce a thumb""" This reverts commit 36d04b517ba5139e1639f2eda28d7f9aaa2dbfb6. * properly obtain giphy metadata regardless of used url * test fixes * adjust gif player * add all twitch embed types * support m.youtube links * few logic adjustments * adjust spotify player height * prefetch gif before showing * use memory-disk cache policy on gifs * use `disk` cachePolicy on ios - can't start/stop animation * support pause/play on web * onLoad fix * remove extra pressable, add accessibility, fix scale issues * improve size of embed * add settings * fix(?) settings * add source to embed player params * update tests * better naming and settings options * consent modal * fix test id * why is webstorm adding .tsx * web modal * simplify types * adjust snap points * remove unnecessary yt embed library. just use the webview always * remove now useless WebGifStill 😭 * more type cleanup * more type cleanup * combine parse and prefs check in one memo * improve dimensions of youtube shorts * oops didn't commit the test 🫥 * add shorts as separate embed type * fix up schema * shorts modal * hide gif details * support localized spotify embeds * more cleanup * improve look and accessibility of gif embeds * Update routing for the external embeds settings page * Update and simplify the external embed preferences screen * Update copy in embedconsent modal and add 'allow all' button --------- Co-authored-by: Hailey <me@haileyok.com>
170 lines
4.9 KiB
TypeScript
170 lines
4.9 KiB
TypeScript
import {EmbedPlayerParams, getGifDims} from 'lib/strings/embed-player'
|
|
import React from 'react'
|
|
import {Image, ImageLoadEventData} from 'expo-image'
|
|
import {
|
|
ActivityIndicator,
|
|
GestureResponderEvent,
|
|
LayoutChangeEvent,
|
|
Pressable,
|
|
StyleSheet,
|
|
View,
|
|
} from 'react-native'
|
|
import {isIOS, isNative, isWeb} from '#/platform/detection'
|
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|
import {useExternalEmbedsPrefs} from 'state/preferences'
|
|
import {useModalControls} from 'state/modals'
|
|
import {useLingui} from '@lingui/react'
|
|
import {msg} from '@lingui/macro'
|
|
import {AppBskyEmbedExternal} from '@atproto/api'
|
|
|
|
export function ExternalGifEmbed({
|
|
link,
|
|
params,
|
|
}: {
|
|
link: AppBskyEmbedExternal.ViewExternal
|
|
params: EmbedPlayerParams
|
|
}) {
|
|
const externalEmbedsPrefs = useExternalEmbedsPrefs()
|
|
const {openModal} = useModalControls()
|
|
const {_} = useLingui()
|
|
|
|
const thumbHasLoaded = React.useRef(false)
|
|
const viewWidth = React.useRef(0)
|
|
|
|
// Tracking if the placer has been activated
|
|
const [isPlayerActive, setIsPlayerActive] = React.useState(false)
|
|
// Tracking whether the gif has been loaded yet
|
|
const [isPrefetched, setIsPrefetched] = React.useState(false)
|
|
// Tracking whether the image is animating
|
|
const [isAnimating, setIsAnimating] = React.useState(true)
|
|
const [imageDims, setImageDims] = React.useState({height: 100, width: 1})
|
|
|
|
// Used for controlling animation
|
|
const imageRef = React.useRef<Image>(null)
|
|
|
|
const load = React.useCallback(() => {
|
|
setIsPlayerActive(true)
|
|
Image.prefetch(params.playerUri).then(() => {
|
|
// Replace the image once it's fetched
|
|
setIsPrefetched(true)
|
|
})
|
|
}, [params.playerUri])
|
|
|
|
const onPlayPress = React.useCallback(
|
|
(event: GestureResponderEvent) => {
|
|
// Don't propagate on web
|
|
event.preventDefault()
|
|
|
|
// Show consent if this is the first load
|
|
if (externalEmbedsPrefs?.[params.source] === undefined) {
|
|
openModal({
|
|
name: 'embed-consent',
|
|
source: params.source,
|
|
onAccept: load,
|
|
})
|
|
return
|
|
}
|
|
// If the player isn't active, we want to activate it and prefetch the gif
|
|
if (!isPlayerActive) {
|
|
load()
|
|
return
|
|
}
|
|
// Control animation on native
|
|
setIsAnimating(prev => {
|
|
if (prev) {
|
|
if (isNative) {
|
|
imageRef.current?.stopAnimating()
|
|
}
|
|
return false
|
|
} else {
|
|
if (isNative) {
|
|
imageRef.current?.startAnimating()
|
|
}
|
|
return true
|
|
}
|
|
})
|
|
},
|
|
[externalEmbedsPrefs, isPlayerActive, load, openModal, params.source],
|
|
)
|
|
|
|
const onLoad = React.useCallback((e: ImageLoadEventData) => {
|
|
if (thumbHasLoaded.current) return
|
|
setImageDims(getGifDims(e.source.height, e.source.width, viewWidth.current))
|
|
thumbHasLoaded.current = true
|
|
}, [])
|
|
|
|
const onLayout = React.useCallback((e: LayoutChangeEvent) => {
|
|
viewWidth.current = e.nativeEvent.layout.width
|
|
}, [])
|
|
|
|
return (
|
|
<Pressable
|
|
style={[
|
|
{height: imageDims.height},
|
|
styles.topRadius,
|
|
styles.gifContainer,
|
|
]}
|
|
onPress={onPlayPress}
|
|
onLayout={onLayout}
|
|
accessibilityRole="button"
|
|
accessibilityHint={_(msg`Plays the GIF`)}
|
|
accessibilityLabel={_(msg`Play ${link.title}`)}>
|
|
{(!isPrefetched || !isAnimating) && ( // If we have not loaded or are not animating, show the overlay
|
|
<View style={[styles.layer, styles.overlayLayer]}>
|
|
<View style={[styles.overlayContainer, styles.topRadius]}>
|
|
{!isAnimating || !isPlayerActive ? ( // Play button when not animating or not active
|
|
<FontAwesomeIcon icon="play" size={42} color="white" />
|
|
) : (
|
|
// Activity indicator while gif loads
|
|
<ActivityIndicator size="large" color="white" />
|
|
)}
|
|
</View>
|
|
</View>
|
|
)}
|
|
<Image
|
|
source={{
|
|
uri:
|
|
!isPrefetched || (isWeb && !isAnimating)
|
|
? link.thumb
|
|
: params.playerUri,
|
|
}} // Web uses the thumb to control playback
|
|
style={{flex: 1}}
|
|
ref={imageRef}
|
|
onLoad={onLoad}
|
|
autoplay={isAnimating}
|
|
contentFit="contain"
|
|
accessibilityIgnoresInvertColors
|
|
accessibilityLabel={link.title}
|
|
accessibilityHint={link.title}
|
|
cachePolicy={isIOS ? 'disk' : 'memory-disk'} // cant control playback with memory-disk on ios
|
|
/>
|
|
</Pressable>
|
|
)
|
|
}
|
|
|
|
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,
|
|
},
|
|
gifContainer: {
|
|
width: '100%',
|
|
overflow: 'hidden',
|
|
},
|
|
})
|