forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useCallback, useEffect, useMemo} from 'react'
2import {ScrollView, View} from 'react-native'
3import {AppBskyEmbedVideo, AtUri} from '@atproto/api'
4import {msg, Trans} from '@lingui/macro'
5import {useLingui} from '@lingui/react'
6import {useQueryClient} from '@tanstack/react-query'
7
8import {VIDEO_FEED_URI} from '#/lib/constants'
9import {makeCustomFeedLink} from '#/lib/routes/links'
10import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
11import {useTrendingSettingsApi} from '#/state/preferences/trending'
12import {RQKEY, usePostFeedQuery} from '#/state/queries/post-feed'
13import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
14import {atoms as a, useGutters, useTheme} from '#/alf'
15import {Button, ButtonIcon} from '#/components/Button'
16import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
17import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
18import {Link} from '#/components/Link'
19import * as Prompt from '#/components/Prompt'
20import {Text} from '#/components/Typography'
21import {
22 CompactVideoPostCard,
23 CompactVideoPostCardPlaceholder,
24} from '#/components/VideoPostCard'
25import {useAnalytics} from '#/analytics'
26
27const CARD_WIDTH = 108
28
29const FEED_DESC = `feedgen|${VIDEO_FEED_URI}`
30const FEED_PARAMS: {
31 feedCacheKey: 'discover'
32} = {
33 feedCacheKey: 'discover',
34}
35
36export function TrendingVideos() {
37 const t = useTheme()
38 const {_} = useLingui()
39 const ax = useAnalytics()
40 const gutters = useGutters([0, 'base'])
41 const {data, isLoading, error} = usePostFeedQuery(FEED_DESC, FEED_PARAMS)
42
43 // Refetch on unmount if nothing else is using this query.
44 const queryClient = useQueryClient()
45 useEffect(() => {
46 return () => {
47 const query = queryClient
48 .getQueryCache()
49 .find({queryKey: RQKEY(FEED_DESC, FEED_PARAMS)})
50 if (query && query.getObserversCount() <= 1) {
51 query.fetch()
52 }
53 }
54 }, [queryClient])
55
56 const {setTrendingVideoDisabled} = useTrendingSettingsApi()
57 const trendingPrompt = Prompt.usePromptControl()
58
59 const onConfirmHide = useCallback(() => {
60 setTrendingVideoDisabled(true)
61 ax.metric('trendingVideos:hide', {context: 'interstitial:discover'})
62 }, [ax, setTrendingVideoDisabled])
63
64 if (error) {
65 return null
66 }
67
68 return (
69 <View
70 style={[
71 a.pt_sm,
72 a.pb_lg,
73 a.border_t,
74 a.overflow_hidden,
75 t.atoms.border_contrast_low,
76 t.atoms.bg_contrast_25,
77 ]}>
78 <View
79 style={[
80 gutters,
81 a.pb_sm,
82 a.flex_row,
83 a.align_center,
84 a.justify_between,
85 ]}>
86 <Text style={[a.text_sm, a.font_semi_bold, a.leading_snug]}>
87 <Trans>Trending Videos</Trans>
88 </Text>
89 <Button
90 label={_(msg`Dismiss this section`)}
91 size="tiny"
92 variant="solid"
93 color="secondary"
94 shape="square"
95 onPress={() => trendingPrompt.open()}>
96 <ButtonIcon icon={X} size="sm" />
97 </Button>
98 </View>
99
100 <BlockDrawerGesture>
101 <ScrollView
102 horizontal
103 showsHorizontalScrollIndicator={false}
104 decelerationRate="fast"
105 snapToInterval={CARD_WIDTH + a.gap_md.gap}
106 style={[a.overflow_visible]}>
107 <View
108 style={[
109 a.flex_row,
110 a.gap_md,
111 {
112 paddingLeft: gutters.paddingLeft,
113 paddingRight: gutters.paddingRight,
114 },
115 ]}>
116 {isLoading ? (
117 Array(10)
118 .fill(0)
119 .map((_, i) => (
120 <View key={i} style={[{width: CARD_WIDTH}]}>
121 <CompactVideoPostCardPlaceholder />
122 </View>
123 ))
124 ) : error || !data ? (
125 <Text>
126 <Trans>Whoops! Trending videos failed to load.</Trans>
127 </Text>
128 ) : (
129 <VideoCards data={data} />
130 )}
131 </View>
132 </ScrollView>
133 </BlockDrawerGesture>
134
135 <Prompt.Basic
136 control={trendingPrompt}
137 title={_(msg`Hide trending videos?`)}
138 description={_(msg`You can update this later from your settings.`)}
139 confirmButtonCta={_(msg`Hide`)}
140 onConfirm={onConfirmHide}
141 />
142 </View>
143 )
144}
145
146function VideoCards({
147 data,
148}: {
149 data: Exclude<ReturnType<typeof usePostFeedQuery>['data'], undefined>
150}) {
151 const ax = useAnalytics()
152 const items = useMemo(() => {
153 return data.pages
154 .flatMap(page => page.slices)
155 .map(slice => slice.items[0])
156 .filter(Boolean)
157 .filter(item => AppBskyEmbedVideo.isView(item.post.embed))
158 .slice(0, 8)
159 }, [data])
160
161 return (
162 <>
163 {items.map(item => (
164 <View key={item.post.uri} style={[{width: CARD_WIDTH}]}>
165 <CompactVideoPostCard
166 post={item.post}
167 moderation={item.moderation}
168 sourceContext={{
169 type: 'feedgen',
170 uri: VIDEO_FEED_URI,
171 sourceInterstitial: 'discover',
172 }}
173 onInteract={() => {
174 ax.metric('videoCard:click', {
175 context: 'interstitial:discover',
176 })
177 }}
178 />
179 </View>
180 ))}
181
182 <ViewMoreCard />
183 </>
184 )
185}
186
187function ViewMoreCard() {
188 const t = useTheme()
189 const {_} = useLingui()
190
191 const href = useMemo(() => {
192 const urip = new AtUri(VIDEO_FEED_URI)
193 return makeCustomFeedLink(urip.host, urip.rkey, undefined, 'discover')
194 }, [])
195
196 const enableSquareButtons = useEnableSquareButtons()
197
198 return (
199 <View style={[{width: CARD_WIDTH * 2}]}>
200 <Link
201 to={href}
202 label={_(msg`View more`)}
203 style={[
204 a.justify_center,
205 a.align_center,
206 a.flex_1,
207 a.rounded_lg,
208 a.border,
209 t.atoms.border_contrast_low,
210 t.atoms.bg,
211 t.atoms.shadow_sm,
212 ]}>
213 {({pressed}) => (
214 <View
215 style={[
216 a.flex_row,
217 a.align_center,
218 a.gap_md,
219 {
220 opacity: pressed ? 0.6 : 1,
221 },
222 ]}>
223 <Text style={[a.text_md]}>
224 <Trans>View more</Trans>
225 </Text>
226 <Button
227 color="primary"
228 size="small"
229 shape={enableSquareButtons ? 'square' : 'round'}
230 label={_(msg`View more trending videos`)}>
231 <ButtonIcon icon={ChevronRight} />
232 </Button>
233 </View>
234 )}
235 </Link>
236 </View>
237 )
238}