Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

at main 345 lines 11 kB view raw
1import {useCallback, useEffect, useLayoutEffect, useMemo, useRef} from 'react' 2import {ActivityIndicator, StyleSheet} from 'react-native' 3import {useFocusEffect} from '@react-navigation/native' 4 5import {PROD_DEFAULT_FEED} from '#/lib/constants' 6import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 7import {useOTAUpdates} from '#/lib/hooks/useOTAUpdates' 8import {useSetTitle} from '#/lib/hooks/useSetTitle' 9import {useRequestNotificationsPermission} from '#/lib/notifications/notifications' 10import { 11 type HomeTabNavigatorParams, 12 type NativeStackScreenProps, 13} from '#/lib/routes/types' 14import {emitSoftReset} from '#/state/events' 15import { 16 type SavedFeedSourceInfo, 17 usePinnedFeedsInfos, 18} from '#/state/queries/feed' 19import {type FeedDescriptor, type FeedParams} from '#/state/queries/post-feed' 20import {usePreferencesQuery} from '#/state/queries/preferences' 21import {type UsePreferencesQueryResponse} from '#/state/queries/preferences/types' 22import {useSession} from '#/state/session' 23import {useSetMinimalShellMode} from '#/state/shell' 24import {useLoggedOutViewControls} from '#/state/shell/logged-out' 25import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed' 26import {FeedPage} from '#/view/com/feeds/FeedPage' 27import {HomeHeader} from '#/view/com/home/HomeHeader' 28import { 29 Pager, 30 type PagerRef, 31 type RenderTabBarFnProps, 32} from '#/view/com/pager/Pager' 33import {CustomFeedEmptyState} from '#/view/com/posts/CustomFeedEmptyState' 34import {FollowingEmptyState} from '#/view/com/posts/FollowingEmptyState' 35import {FollowingEndOfFeed} from '#/view/com/posts/FollowingEndOfFeed' 36import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned' 37import {useTheme} from '#/alf' 38import * as Layout from '#/components/Layout' 39import {useAnalytics} from '#/analytics' 40import {IS_LIQUID_GLASS, IS_WEB} from '#/env' 41import {useDemoMode} from '#/storage/hooks/demo-mode' 42 43type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home' | 'Start'> 44export function HomeScreen(props: Props) { 45 const {setShowLoggedOut} = useLoggedOutViewControls() 46 const {data: preferences} = usePreferencesQuery() 47 const {currentAccount} = useSession() 48 const {data: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} = 49 usePinnedFeedsInfos() 50 const t = useTheme() 51 52 useEffect(() => { 53 if (IS_WEB && !currentAccount) { 54 const getParams = new URLSearchParams(window.location.search) 55 const splash = getParams.get('splash') 56 if (splash === 'true') { 57 setShowLoggedOut(true) 58 return 59 } 60 } 61 62 const params = props.route.params 63 if ( 64 currentAccount && 65 props.route.name === 'Start' && 66 params?.name && 67 params?.rkey 68 ) { 69 props.navigation.navigate('StarterPack', { 70 rkey: params.rkey, 71 name: params.name, 72 }) 73 } 74 }, [ 75 currentAccount, 76 props.navigation, 77 props.route.name, 78 props.route.params, 79 setShowLoggedOut, 80 ]) 81 82 if (preferences && pinnedFeedInfos && !isPinnedFeedsLoading) { 83 return ( 84 <Layout.Screen testID="HomeScreen" noInsetTop={IS_LIQUID_GLASS}> 85 <HomeScreenReady 86 {...props} 87 preferences={preferences} 88 pinnedFeedInfos={pinnedFeedInfos} 89 /> 90 </Layout.Screen> 91 ) 92 } else { 93 return ( 94 <Layout.Screen> 95 <Layout.Center style={styles.loading}> 96 <ActivityIndicator size="large" color={t.palette.primary_500} /> 97 </Layout.Center> 98 </Layout.Screen> 99 ) 100 } 101} 102 103function HomeScreenReady({ 104 preferences, 105 pinnedFeedInfos, 106}: Props & { 107 preferences: UsePreferencesQueryResponse 108 pinnedFeedInfos: SavedFeedSourceInfo[] 109}) { 110 const ax = useAnalytics() 111 const allFeeds = useMemo( 112 () => pinnedFeedInfos.map(f => f.feedDescriptor), 113 [pinnedFeedInfos], 114 ) 115 const maybeRawSelectedFeed: FeedDescriptor | undefined = 116 useSelectedFeed() ?? allFeeds[0] 117 const setSelectedFeed = useSetSelectedFeed() 118 const maybeFoundIndex = allFeeds.indexOf(maybeRawSelectedFeed) 119 const selectedIndex = Math.max(0, maybeFoundIndex) 120 const maybeSelectedFeed: FeedDescriptor | undefined = allFeeds[selectedIndex] 121 const requestNotificationsPermission = useRequestNotificationsPermission() 122 123 useSetTitle(pinnedFeedInfos[selectedIndex]?.displayName) 124 useOTAUpdates() 125 126 useEffect(() => { 127 requestNotificationsPermission('Home') 128 }, [requestNotificationsPermission]) 129 130 const pagerRef = useRef<PagerRef>(null) 131 const lastPagerReportedIndexRef = useRef(selectedIndex) 132 useLayoutEffect(() => { 133 // Since the pager is not a controlled component, adjust it imperatively 134 // if the selected index gets out of sync with what it last reported. 135 // This is supposed to only happen on the web when you use the right nav. 136 if (selectedIndex !== lastPagerReportedIndexRef.current) { 137 lastPagerReportedIndexRef.current = selectedIndex 138 pagerRef.current?.setPage(selectedIndex) 139 } 140 }, [selectedIndex]) 141 142 const {hasSession} = useSession() 143 const setMinimalShellMode = useSetMinimalShellMode() 144 useFocusEffect( 145 useCallback(() => { 146 setMinimalShellMode(false) 147 }, [setMinimalShellMode]), 148 ) 149 150 useFocusEffect( 151 useNonReactiveCallback(() => { 152 if (maybeSelectedFeed) { 153 ax.metric('home:feedDisplayed', { 154 index: selectedIndex, 155 feedType: maybeSelectedFeed.split('|')[0], 156 feedUrl: maybeSelectedFeed, 157 reason: 'focus', 158 }) 159 } 160 }), 161 ) 162 163 const onPageSelected = useCallback( 164 (index: number) => { 165 setMinimalShellMode(false) 166 const maybeFeed = allFeeds[index] 167 168 // Mutate the ref before setting state to avoid the imperative syncing effect 169 // above from starting a loop on Android when swiping back and forth. 170 lastPagerReportedIndexRef.current = index 171 setSelectedFeed(maybeFeed) 172 173 if (maybeFeed) { 174 ax.metric('home:feedDisplayed', { 175 index, 176 feedType: maybeFeed.split('|')[0], 177 feedUrl: maybeFeed, 178 }) 179 } 180 }, 181 [ax, setSelectedFeed, setMinimalShellMode, allFeeds], 182 ) 183 184 const onPressSelected = useCallback(() => { 185 emitSoftReset() 186 }, []) 187 188 const onPageScrollStateChanged = useCallback( 189 (state: 'idle' | 'dragging' | 'settling') => { 190 'worklet' 191 if (state === 'dragging') { 192 setMinimalShellMode(false) 193 } 194 }, 195 [setMinimalShellMode], 196 ) 197 198 const [demoMode] = useDemoMode() 199 200 const renderTabBar = useCallback( 201 (props: RenderTabBarFnProps) => { 202 if (demoMode) { 203 return ( 204 <HomeHeader 205 key="FEEDS_TAB_BAR" 206 {...props} 207 testID="homeScreenFeedTabs" 208 onPressSelected={onPressSelected} 209 // @ts-ignore 210 feeds={[{displayName: 'Following'}, {displayName: 'Discover'}]} 211 /> 212 ) 213 } 214 return ( 215 <HomeHeader 216 key="FEEDS_TAB_BAR" 217 {...props} 218 testID="homeScreenFeedTabs" 219 onPressSelected={onPressSelected} 220 feeds={pinnedFeedInfos} 221 /> 222 ) 223 }, 224 [onPressSelected, pinnedFeedInfos, demoMode], 225 ) 226 227 const renderFollowingEmptyState = useCallback(() => { 228 return <FollowingEmptyState /> 229 }, []) 230 231 const renderCustomFeedEmptyState = useCallback(() => { 232 return <CustomFeedEmptyState /> 233 }, []) 234 235 const homeFeedParams = useMemo<FeedParams>(() => { 236 return { 237 mergeFeedEnabled: Boolean(preferences.feedViewPrefs.lab_mergeFeedEnabled), 238 mergeFeedSources: preferences.feedViewPrefs.lab_mergeFeedEnabled 239 ? preferences.savedFeeds 240 .filter(f => f.type === 'feed' || f.type === 'list') 241 .map(f => f.value) 242 : [], 243 } 244 }, [preferences]) 245 246 if (demoMode) { 247 return ( 248 <Pager 249 ref={pagerRef} 250 testID="homeScreen" 251 onPageSelected={onPageSelected} 252 onPageScrollStateChanged={onPageScrollStateChanged} 253 renderTabBar={renderTabBar} 254 initialPage={selectedIndex}> 255 <FeedPage 256 testID="demoFeedPage" 257 isPageFocused 258 isPageAdjacent={false} 259 feed="demo" 260 renderEmptyState={renderCustomFeedEmptyState} 261 feedInfo={pinnedFeedInfos[0]} 262 /> 263 <FeedPage 264 testID="customFeedPage" 265 isPageFocused 266 isPageAdjacent={false} 267 feed={`feedgen|${PROD_DEFAULT_FEED('whats-hot')}`} 268 renderEmptyState={renderCustomFeedEmptyState} 269 feedInfo={pinnedFeedInfos[0]} 270 /> 271 </Pager> 272 ) 273 } 274 275 return hasSession ? ( 276 <Pager 277 key={allFeeds.join(',')} 278 ref={pagerRef} 279 testID="homeScreen" 280 initialPage={selectedIndex} 281 onPageSelected={onPageSelected} 282 onPageScrollStateChanged={onPageScrollStateChanged} 283 renderTabBar={renderTabBar}> 284 {pinnedFeedInfos.length ? ( 285 pinnedFeedInfos.map((feedInfo, index) => { 286 const feed = feedInfo.feedDescriptor 287 if (feed === 'following') { 288 return ( 289 <FeedPage 290 key={feed} 291 testID="followingFeedPage" 292 isPageFocused={maybeSelectedFeed === feed} 293 isPageAdjacent={Math.abs(selectedIndex - index) === 1} 294 feed={feed} 295 feedParams={homeFeedParams} 296 renderEmptyState={renderFollowingEmptyState} 297 renderEndOfFeed={FollowingEndOfFeed} 298 feedInfo={feedInfo} 299 /> 300 ) 301 } 302 const savedFeedConfig = feedInfo.savedFeed 303 return ( 304 <FeedPage 305 key={feed} 306 testID="customFeedPage" 307 isPageFocused={maybeSelectedFeed === feed} 308 isPageAdjacent={Math.abs(selectedIndex - index) === 1} 309 feed={feed} 310 renderEmptyState={renderCustomFeedEmptyState} 311 savedFeedConfig={savedFeedConfig} 312 feedInfo={feedInfo} 313 /> 314 ) 315 }) 316 ) : ( 317 <NoFeedsPinned preferences={preferences} /> 318 )} 319 </Pager> 320 ) : ( 321 <Pager 322 testID="homeScreen" 323 onPageSelected={onPageSelected} 324 onPageScrollStateChanged={onPageScrollStateChanged} 325 renderTabBar={renderTabBar}> 326 <FeedPage 327 testID="customFeedPage" 328 isPageFocused 329 isPageAdjacent={false} 330 feed={`feedgen|${PROD_DEFAULT_FEED('whats-hot')}`} 331 renderEmptyState={renderCustomFeedEmptyState} 332 feedInfo={pinnedFeedInfos[0]} 333 /> 334 </Pager> 335 ) 336} 337 338const styles = StyleSheet.create({ 339 loading: { 340 height: '100%', 341 alignContent: 'center', 342 justifyContent: 'center', 343 paddingBottom: 100, 344 }, 345})