forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 💫
1import {useCallback, useRef, useState} from 'react'
2import {ActivityIndicator, View} from 'react-native'
3import {ImageBackground} from 'expo-image'
4import {type AppBskyEmbedVideo} from '@atproto/api'
5import {msg, Trans} from '@lingui/macro'
6import {useLingui} from '@lingui/react'
7
8import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
9import {atoms as a, platform} from '#/alf'
10import {Button} from '#/components/Button'
11import {useThrottledValue} from '#/components/hooks/useThrottledValue'
12import {ConstrainedImage} from '#/components/images/AutoSizedImage'
13import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
14import {GifPresentationControls} from './GifPresentationControls'
15import {VideoEmbedInnerNative} from './VideoEmbedInner/VideoEmbedInnerNative'
16import * as VideoFallback from './VideoEmbedInner/VideoFallback'
17
18interface Props {
19 embed: AppBskyEmbedVideo.View
20}
21
22export function VideoEmbed({embed}: Props) {
23 const [key, setKey] = useState(0)
24
25 const renderError = useCallback(
26 (error: unknown) => (
27 <VideoError error={error} retry={() => setKey(key + 1)} />
28 ),
29 [key],
30 )
31
32 let aspectRatio: number | undefined
33 const dims = embed.aspectRatio
34 if (dims) {
35 aspectRatio = dims.width / dims.height
36 if (Number.isNaN(aspectRatio)) {
37 aspectRatio = undefined
38 }
39 }
40
41 let constrained: number | undefined
42 if (aspectRatio !== undefined) {
43 const ratio = 1 / 2 // max of 1:2 ratio in feeds
44 constrained = Math.max(aspectRatio, ratio)
45 }
46
47 const contents = (
48 <ErrorBoundary renderError={renderError} key={key}>
49 <InnerWrapper embed={embed} />
50 </ErrorBoundary>
51 )
52
53 return (
54 <View style={[a.pt_xs]}>
55 <ConstrainedImage
56 aspectRatio={constrained || 1}
57 // slightly smaller max height than images
58 // images use 16 / 9, for reference
59 minMobileAspectRatio={14 / 9}>
60 {contents}
61 </ConstrainedImage>
62 </View>
63 )
64}
65
66function InnerWrapper({embed}: Props) {
67 const {_} = useLingui()
68 const ref = useRef<{togglePlayback: () => void}>(null)
69
70 const [status, setStatus] = useState<'playing' | 'paused' | 'pending'>(
71 'pending',
72 )
73 const [isLoading, setIsLoading] = useState(false)
74 const [isActive, setIsActive] = useState(false)
75 const showSpinner = useThrottledValue(isActive && isLoading, 100)
76
77 const showOverlay =
78 !isActive ||
79 isLoading ||
80 (status === 'paused' && !isActive) ||
81 status === 'pending'
82
83 if (!isActive && status !== 'pending') {
84 setStatus('pending')
85 }
86
87 return (
88 <>
89 <VideoEmbedInnerNative
90 embed={embed}
91 setStatus={setStatus}
92 setIsLoading={setIsLoading}
93 setIsActive={setIsActive}
94 ref={ref}
95 />
96 <ImageBackground
97 source={{uri: embed.thumbnail}}
98 accessibilityIgnoresInvertColors
99 style={[
100 a.absolute,
101 a.inset_0,
102 {
103 backgroundColor: 'transparent', // If you don't add `backgroundColor` to the styles here,
104 // the play button won't show up on the first render on android 🥴😮💨
105 },
106 platform({
107 android: {display: showOverlay ? 'flex' : 'none'},
108 ios: {zIndex: showOverlay ? 1 : -1},
109 }),
110 ]}
111 cachePolicy="memory-disk" // Preferring memory cache helps to avoid flicker when re-displaying on android
112 >
113 {showOverlay &&
114 (embed.presentation === 'gif' ? (
115 <GifPresentationControls
116 isPlaying={false}
117 isLoading={showSpinner}
118 onPress={() => {
119 ref.current?.togglePlayback()
120 }}
121 altText={embed.alt}
122 />
123 ) : (
124 <Button
125 style={[a.flex_1, a.align_center, a.justify_center]}
126 onPress={() => {
127 ref.current?.togglePlayback()
128 }}
129 label={_(msg`Play video`)}>
130 {showSpinner ? (
131 <View style={[a.align_center, a.justify_center]}>
132 <ActivityIndicator size="large" color="white" />
133 </View>
134 ) : (
135 <PlayButtonIcon />
136 )}
137 </Button>
138 ))}
139 </ImageBackground>
140 </>
141 )
142}
143
144function VideoError({retry}: {error: unknown; retry: () => void}) {
145 return (
146 <VideoFallback.Container>
147 <VideoFallback.Text>
148 <Trans>
149 An error occurred while loading the video. Please try again later.
150 </Trans>
151 </VideoFallback.Text>
152 <VideoFallback.RetryButton onPress={retry} />
153 </VideoFallback.Container>
154 )
155}