Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[D1X] Integrate interstitials (#4698)

* Use discriminated union

* Integrate interstitials

* Add gates and handling for variants

* Only show interstitials for logged in accounts since flags are based on user ID

* Nit

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>

authored by

Eric Bailey
Dan Abramov
and committed by
GitHub
04cfd066 0598fc2f

+198 -35
+4 -4
src/components/FeedInterstitials.tsx
··· 203 203 {content} 204 204 205 205 <Button 206 - label={_(msg`Browse more accounts on our explore page`)} 206 + label={_(msg`Browse more accounts on the Explore page`)} 207 207 onPress={() => { 208 208 navigation.navigate('SearchTab') 209 209 }}> ··· 211 211 <View style={[a.flex_1, a.justify_center]}> 212 212 <View style={[a.flex_row, a.px_lg]}> 213 213 <Text style={[a.pr_xl, a.flex_1, a.leading_snug]}> 214 - <Trans>Browse more suggestions on our explore page</Trans> 214 + <Trans>Browse more suggestions on the Explore page</Trans> 215 215 </Text> 216 216 217 217 <Arrow size="xl" /> ··· 329 329 {content} 330 330 331 331 <Button 332 - label={_(msg`Browse more feeds on our explore page`)} 332 + label={_(msg`Browse more feeds on the Explore page`)} 333 333 onPress={() => { 334 334 navigation.navigate('SearchTab') 335 335 }} ··· 338 338 <View style={[a.flex_1, a.justify_center]}> 339 339 <View style={[a.flex_row, a.px_lg]}> 340 340 <Text style={[a.pr_xl, a.flex_1, a.leading_snug]}> 341 - <Trans>Browse more suggestions on our explore page</Trans> 341 + <Trans>Browse more suggestions on the Explore page</Trans> 342 342 </Text> 343 343 344 344 <Arrow size="xl" />
+2
src/lib/statsig/gates.ts
··· 6 6 | 'request_notifications_permission_after_onboarding_v2' 7 7 | 'show_avi_follow_button' 8 8 | 'show_follow_back_label_v2' 9 + | 'suggested_feeds_interstitial' 10 + | 'suggested_follows_interstitial'
+192 -31
src/view/com/posts/Feed.tsx
··· 15 15 import {useQueryClient} from '@tanstack/react-query' 16 16 17 17 import {FALLBACK_MARKER_POST} from '#/lib/api/feed/home' 18 - import {KNOWN_SHUTDOWN_FEEDS} from '#/lib/constants' 19 - import {logEvent} from '#/lib/statsig/statsig' 18 + import {DISCOVER_FEED_URI, KNOWN_SHUTDOWN_FEEDS} from '#/lib/constants' 19 + import {logEvent, useGate} from '#/lib/statsig/statsig' 20 20 import {logger} from '#/logger' 21 21 import {isWeb} from '#/platform/detection' 22 22 import {listenPostCreated} from '#/state/events' ··· 25 25 import { 26 26 FeedDescriptor, 27 27 FeedParams, 28 + FeedPostSlice, 28 29 pollLatest, 29 30 RQKEY, 30 31 usePostFeedQuery, ··· 33 34 import {useAnalytics} from 'lib/analytics/analytics' 34 35 import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender' 35 36 import {useTheme} from 'lib/ThemeContext' 37 + import {SuggestedFeeds, SuggestedFollows} from '#/components/FeedInterstitials' 36 38 import {List, ListRef} from '../util/List' 37 39 import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' 38 40 import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' ··· 41 43 import {FeedShutdownMsg} from './FeedShutdownMsg' 42 44 import {FeedSlice} from './FeedSlice' 43 45 44 - const LOADING_ITEM = {_reactKey: '__loading__'} 45 - const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} 46 - const ERROR_ITEM = {_reactKey: '__error__'} 47 - const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'} 48 - const FEED_SHUTDOWN_MSG_ITEM = {_reactKey: '__feed_shutdown_msg_item__'} 46 + type FeedItem = 47 + | { 48 + type: 'loading' 49 + key: string 50 + } 51 + | { 52 + type: 'empty' 53 + key: string 54 + } 55 + | { 56 + type: 'error' 57 + key: string 58 + } 59 + | { 60 + type: 'loadMoreError' 61 + key: string 62 + } 63 + | { 64 + type: 'feedShutdownMsg' 65 + key: string 66 + } 67 + | { 68 + type: 'slice' 69 + key: string 70 + slice: FeedPostSlice 71 + } 72 + | { 73 + type: 'interstitialFeeds' 74 + key: string 75 + params: { 76 + variant: 'default' | string 77 + } 78 + slot: number 79 + } 80 + | { 81 + type: 'interstitialFollows' 82 + key: string 83 + params: { 84 + variant: 'default' | string 85 + } 86 + slot: number 87 + } 88 + 89 + const feedInterstitialType = 'interstitialFeeds' 90 + const followInterstitialType = 'interstitialFollows' 91 + const interstials: Record< 92 + 'following' | 'discover', 93 + (FeedItem & {type: 'interstitialFeeds' | 'interstitialFollows'})[] 94 + > = { 95 + following: [ 96 + { 97 + type: followInterstitialType, 98 + params: { 99 + variant: 'default', 100 + }, 101 + key: followInterstitialType, 102 + slot: 20, 103 + }, 104 + { 105 + type: feedInterstitialType, 106 + params: { 107 + variant: 'default', 108 + }, 109 + key: feedInterstitialType, 110 + slot: 40, 111 + }, 112 + ], 113 + discover: [ 114 + { 115 + type: feedInterstitialType, 116 + params: { 117 + variant: 'default', 118 + }, 119 + key: feedInterstitialType, 120 + slot: 20, 121 + }, 122 + { 123 + type: followInterstitialType, 124 + params: { 125 + variant: 'default', 126 + }, 127 + key: followInterstitialType, 128 + slot: 40, 129 + }, 130 + ], 131 + } 49 132 50 133 // DISABLED need to check if this is causing random feed refreshes -prf 51 134 // const REFRESH_AFTER = STALE.HOURS.ONE ··· 94 177 const {track} = useAnalytics() 95 178 const {_} = useLingui() 96 179 const queryClient = useQueryClient() 97 - const {currentAccount} = useSession() 180 + const {currentAccount, hasSession} = useSession() 98 181 const initialNumToRender = useInitialNumToRender() 99 182 const feedFeedback = useFeedFeedbackContext() 100 183 const [isPTRing, setIsPTRing] = React.useState(false) 101 184 const checkForNewRef = React.useRef<(() => void) | null>(null) 102 185 const lastFetchRef = React.useRef<number>(Date.now()) 103 186 const [feedType, feedUri] = feed.split('|') 187 + const feedIsDiscover = feedUri === DISCOVER_FEED_URI 188 + const feedIsFollowing = feedType === 'following' 189 + const gate = useGate() 104 190 105 191 const opts = React.useMemo( 106 192 () => ({enabled, ignoreFilterFor}), ··· 198 284 } 199 285 }, [pollInterval]) 200 286 201 - const feedItems = React.useMemo(() => { 202 - let arr: any[] = [] 287 + const feedItems: FeedItem[] = React.useMemo(() => { 288 + let arr: FeedItem[] = [] 203 289 if (KNOWN_SHUTDOWN_FEEDS.includes(feedUri)) { 204 - arr = arr.concat([FEED_SHUTDOWN_MSG_ITEM]) 290 + arr.push({ 291 + type: 'feedShutdownMsg', 292 + key: 'feedShutdownMsg', 293 + }) 205 294 } 206 295 if (isFetched) { 207 296 if (isError && isEmpty) { 208 - arr = arr.concat([ERROR_ITEM]) 297 + arr.push({ 298 + type: 'error', 299 + key: 'error', 300 + }) 209 301 } else if (isEmpty) { 210 - arr = arr.concat([EMPTY_FEED_ITEM]) 302 + arr.push({ 303 + type: 'empty', 304 + key: 'empty', 305 + }) 211 306 } else if (data) { 212 307 for (const page of data?.pages) { 213 - arr = arr.concat(page.slices) 308 + arr = arr.concat( 309 + page.slices.map(s => ({ 310 + type: 'slice', 311 + slice: s, 312 + key: s._reactKey, 313 + })), 314 + ) 214 315 } 215 316 } 216 317 if (isError && !isEmpty) { 217 - arr = arr.concat([LOAD_MORE_ERROR_ITEM]) 318 + arr.push({ 319 + type: 'loadMoreError', 320 + key: 'loadMoreError', 321 + }) 218 322 } 219 323 } else { 220 - arr.push(LOADING_ITEM) 324 + arr.push({ 325 + type: 'loading', 326 + key: 'loading', 327 + }) 328 + } 329 + 330 + if (hasSession) { 331 + const feedType = feedIsFollowing 332 + ? 'following' 333 + : feedIsDiscover 334 + ? 'discover' 335 + : undefined 336 + 337 + if (feedType) { 338 + for (const interstitial of interstials[feedType]) { 339 + const feedInterstitialEnabled = 340 + interstitial.type === feedInterstitialType && 341 + gate('suggested_feeds_interstitial') 342 + const followInterstitialEnabled = 343 + interstitial.type === followInterstitialType && 344 + gate('suggested_follows_interstitial') 345 + 346 + if (feedInterstitialEnabled || followInterstitialEnabled) { 347 + const variant = 'default' // replace with experiment variant 348 + const int = { 349 + ...interstitial, 350 + params: {variant}, 351 + // overwrite key with unique value 352 + key: [interstitial.type, variant].join(':'), 353 + } 354 + 355 + if (arr.length > interstitial.slot) { 356 + arr.splice(interstitial.slot, 0, int) 357 + } 358 + } 359 + } 360 + } 221 361 } 362 + 222 363 return arr 223 - }, [isFetched, isError, isEmpty, data, feedUri]) 364 + }, [ 365 + isFetched, 366 + isError, 367 + isEmpty, 368 + data, 369 + feedUri, 370 + feedIsDiscover, 371 + feedIsFollowing, 372 + gate, 373 + hasSession, 374 + ]) 224 375 225 376 // events 226 377 // = ··· 280 431 // = 281 432 282 433 const renderItem = React.useCallback( 283 - ({item, index}: ListRenderItemInfo<any>) => { 284 - if (item === EMPTY_FEED_ITEM) { 434 + ({item, index}: ListRenderItemInfo<FeedItem>) => { 435 + if (item.type === 'empty') { 285 436 return renderEmptyState() 286 - } else if (item === ERROR_ITEM) { 437 + } else if (item.type === 'error') { 287 438 return ( 288 439 <FeedErrorMessage 289 440 feedDesc={feed} ··· 292 443 savedFeedConfig={savedFeedConfig} 293 444 /> 294 445 ) 295 - } else if (item === LOAD_MORE_ERROR_ITEM) { 446 + } else if (item.type === 'loadMoreError') { 296 447 return ( 297 448 <LoadMoreRetryBtn 298 449 label={_( ··· 301 452 onPress={onPressRetryLoadMore} 302 453 /> 303 454 ) 304 - } else if (item === LOADING_ITEM) { 455 + } else if (item.type === 'loading') { 305 456 return <PostFeedLoadingPlaceholder /> 306 - } else if (item === FEED_SHUTDOWN_MSG_ITEM) { 457 + } else if (item.type === 'feedShutdownMsg') { 307 458 return <FeedShutdownMsg feedUri={feedUri} /> 308 - } else if (item.rootUri === FALLBACK_MARKER_POST.post.uri) { 309 - // HACK 310 - // tell the user we fell back to discover 311 - // see home.ts (feed api) for more info 312 - // -prf 313 - return <DiscoverFallbackHeader /> 459 + } else if (item.type === feedInterstitialType) { 460 + return <SuggestedFeeds /> 461 + } else if (item.type === followInterstitialType) { 462 + return <SuggestedFollows /> 463 + } else if (item.type === 'slice') { 464 + if (item.slice.rootUri === FALLBACK_MARKER_POST.post.uri) { 465 + // HACK 466 + // tell the user we fell back to discover 467 + // see home.ts (feed api) for more info 468 + // -prf 469 + return <DiscoverFallbackHeader /> 470 + } 471 + return ( 472 + <FeedSlice slice={item.slice} hideTopBorder={index === 0 && !isWeb} /> 473 + ) 474 + } else { 475 + return null 314 476 } 315 - return <FeedSlice slice={item} hideTopBorder={index === 0 && !isWeb} /> 316 477 }, 317 478 [ 318 479 renderEmptyState, ··· 354 515 testID={testID ? `${testID}-flatlist` : undefined} 355 516 ref={scrollElRef} 356 517 data={feedItems} 357 - keyExtractor={item => item._reactKey} 518 + keyExtractor={item => item.key} 358 519 renderItem={renderItem} 359 520 ListFooterComponent={FeedFooter} 360 521 ListHeaderComponent={ListHeaderComponent}