Bluesky app fork with some witchin' additions 馃挮
at readme-update 238 lines 6.7 kB view raw
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}