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