bsky-app/src/view/com/util/post-embeds/ExternalGifEmbed.tsx
Eric Bailey b3381da1c1
Image/video border + tweaks (#5324)
* Image/video border (#5253)

* Update AutoSizedImage.tsx

* Update AutoSizedImage.tsx

* Update Gallery.tsx

* Update ExternalLinkEmbed.tsx

* Update MediaPreview.tsx

* Update UserAvatar.tsx

* Update ExternalLinkEmbed.tsx

* Update ExternalPlayerEmbed.tsx

* Update ExternalGifEmbed.tsx

* Update GifEmbed.tsx

* Update ExternalGifEmbed.tsx

* Update GifEmbed.tsx

* Update UserAvatar.tsx

* Update ExternalPlayerEmbed.tsx

* Update ExternalPlayerEmbed.tsx

* video

* Update QuoteEmbed.tsx

* Tweaks, abstract components

---------

Co-authored-by: Minseo Lee <itoupluk427@gmail.com>
2024-09-13 18:02:58 +01:00

207 lines
5.6 KiB
TypeScript

import React from 'react'
import {
ActivityIndicator,
GestureResponderEvent,
LayoutChangeEvent,
Pressable,
StyleSheet,
} from 'react-native'
import {Image, ImageLoadEventData} from 'expo-image'
import {AppBskyEmbedExternal} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {EmbedPlayerParams, getGifDims} from '#/lib/strings/embed-player'
import {isIOS, isNative, isWeb} from '#/platform/detection'
import {useExternalEmbedsPrefs} from '#/state/preferences'
import {atoms as a, useTheme} from '#/alf'
import {useDialogControl} from '#/components/Dialog'
import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent'
import {Fill} from '#/components/Fill'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
export function ExternalGifEmbed({
link,
params,
}: {
link: AppBskyEmbedExternal.ViewExternal
params: EmbedPlayerParams
}) {
const t = useTheme()
const externalEmbedsPrefs = useExternalEmbedsPrefs()
const {_} = useLingui()
const consentDialogControl = useDialogControl()
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) {
consentDialogControl.open()
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
}
})
},
[
consentDialogControl,
externalEmbedsPrefs,
isPlayerActive,
load,
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 (
<>
<EmbedConsentDialog
control={consentDialogControl}
source={params.source}
onAccept={load}
/>
<Pressable
style={[
{height: imageDims.height},
styles.gifContainer,
a.rounded_sm,
a.overflow_hidden,
{
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
},
]}
onPress={onPlayPress}
onLayout={onLayout}
accessibilityRole="button"
accessibilityHint={_(msg`Plays the GIF`)}
accessibilityLabel={_(msg`Play ${link.title}`)}>
<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
/>
{(!isPrefetched || !isAnimating) && (
<Fill style={[a.align_center, a.justify_center]}>
<Fill
style={[
t.name === 'light' ? t.atoms.bg_contrast_975 : t.atoms.bg,
{
opacity: 0.3,
},
]}
/>
{!isAnimating || !isPlayerActive ? ( // Play button when not animating or not active
<PlayButtonIcon />
) : (
// Activity indicator while gif loads
<ActivityIndicator size="large" color="white" />
)}
</Fill>
)}
<MediaInsetBorder
opaque
style={[
{
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
},
]}
/>
</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',
},
overlayLayer: {
zIndex: 2,
},
gifContainer: {
width: '100%',
overflow: 'hidden',
},
})