forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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})