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 {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
9import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
10import {atoms as a} from '#/alf'
11import {Button} from '#/components/Button'
12import {useThrottledValue} from '#/components/hooks/useThrottledValue'
13import {ConstrainedImage} from '#/components/images/AutoSizedImage'
14import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
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 enableSquareButtons = useEnableSquareButtons()
71
72 const [status, setStatus] = useState<'playing' | 'paused' | 'pending'>(
73 'pending',
74 )
75 const [isLoading, setIsLoading] = useState(false)
76 const [isActive, setIsActive] = useState(false)
77 const showSpinner = useThrottledValue(isActive && isLoading, 100)
78
79 const showOverlay =
80 !isActive ||
81 isLoading ||
82 (status === 'paused' && !isActive) ||
83 status === 'pending'
84
85 if (!isActive && status !== 'pending') {
86 setStatus('pending')
87 }
88
89 return (
90 <>
91 <VideoEmbedInnerNative
92 embed={embed}
93 setStatus={setStatus}
94 setIsLoading={setIsLoading}
95 setIsActive={setIsActive}
96 ref={ref}
97 />
98 <ImageBackground
99 source={{uri: embed.thumbnail}}
100 accessibilityIgnoresInvertColors
101 style={[
102 a.absolute,
103 a.inset_0,
104 {
105 backgroundColor: 'transparent', // If you don't add `backgroundColor` to the styles here,
106 // the play button won't show up on the first render on android 🥴😮💨
107 display: showOverlay ? 'flex' : 'none',
108 },
109 ]}
110 cachePolicy="memory-disk" // Preferring memory cache helps to avoid flicker when re-displaying on android
111 >
112 {showOverlay && (
113 <Button
114 style={[a.flex_1, a.align_center, a.justify_center]}
115 onPress={() => {
116 ref.current?.togglePlayback()
117 }}
118 label={_(msg`Play video`)}>
119 {showSpinner ? (
120 <View
121 style={[
122 enableSquareButtons ? a.rounded_sm : a.rounded_full,
123 a.p_xs,
124 a.align_center,
125 a.justify_center,
126 ]}>
127 <ActivityIndicator size="large" color="white" />
128 </View>
129 ) : (
130 <PlayButtonIcon />
131 )}
132 </Button>
133 )}
134 </ImageBackground>
135 </>
136 )
137}
138
139function VideoError({retry}: {error: unknown; retry: () => void}) {
140 return (
141 <VideoFallback.Container>
142 <VideoFallback.Text>
143 <Trans>
144 An error occurred while loading the video. Please try again later.
145 </Trans>
146 </VideoFallback.Text>
147 <VideoFallback.RetryButton onPress={retry} />
148 </VideoFallback.Container>
149 )
150}