* 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>
207 lines
5.6 KiB
TypeScript
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',
|
|
},
|
|
})
|