forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {
2 type JSX,
3 useCallback,
4 useEffect,
5 useMemo,
6 useRef,
7 useState,
8} from 'react'
9import {View} from 'react-native'
10import {type AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api'
11import {msg} from '@lingui/macro'
12import {useLingui} from '@lingui/react'
13import {type NavigationProp, useNavigation} from '@react-navigation/native'
14import {useQueryClient} from '@tanstack/react-query'
15
16import {DISCOVER_FEED_URI, VIDEO_FEED_URIS} from '#/lib/constants'
17import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
18import {ComposeIcon2} from '#/lib/icons'
19import {getRootNavigation, getTabState, TabState} from '#/lib/routes/helpers'
20import {type AllNavigatorParams} from '#/lib/routes/types'
21import {logEvent} from '#/lib/statsig/statsig'
22import {s} from '#/lib/styles'
23import {isNative} from '#/platform/detection'
24import {listenSoftReset} from '#/state/events'
25import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
26import {useSetHomeBadge} from '#/state/home-badge'
27import {type FeedSourceInfo} from '#/state/queries/feed'
28import {
29 type FeedDescriptor,
30 type FeedParams,
31 RQKEY as FEED_RQKEY,
32} from '#/state/queries/post-feed'
33import {truncateAndInvalidate} from '#/state/queries/util'
34import {useSession} from '#/state/session'
35import {useSetMinimalShellMode} from '#/state/shell'
36import {useHeaderOffset} from '#/components/hooks/useHeaderOffset'
37import {PostFeed} from '../posts/PostFeed'
38import {FAB} from '../util/fab/FAB'
39import {type ListMethods} from '../util/List'
40import {LoadLatestBtn} from '../util/load-latest/LoadLatestBtn'
41import {MainScrollProvider} from '../util/MainScrollProvider'
42
43const POLL_FREQ = 60e3 // 60sec
44
45export function FeedPage({
46 testID,
47 isPageFocused,
48 isPageAdjacent,
49 feed,
50 feedParams,
51 renderEmptyState,
52 renderEndOfFeed,
53 savedFeedConfig,
54 feedInfo,
55}: {
56 testID?: string
57 feed: FeedDescriptor
58 feedParams?: FeedParams
59 isPageFocused: boolean
60 isPageAdjacent: boolean
61 renderEmptyState: () => JSX.Element
62 renderEndOfFeed?: () => JSX.Element
63 savedFeedConfig?: AppBskyActorDefs.SavedFeed
64 feedInfo: FeedSourceInfo
65}) {
66 const {hasSession} = useSession()
67 const {_} = useLingui()
68 const navigation = useNavigation<NavigationProp<AllNavigatorParams>>()
69 const queryClient = useQueryClient()
70 const {openComposer} = useOpenComposer()
71 const [isScrolledDown, setIsScrolledDown] = useState(false)
72 const setMinimalShellMode = useSetMinimalShellMode()
73 const headerOffset = useHeaderOffset()
74 const feedFeedback = useFeedFeedback(feedInfo, hasSession)
75 const scrollElRef = useRef<ListMethods>(null)
76 const [hasNew, setHasNew] = useState(false)
77 const setHomeBadge = useSetHomeBadge()
78 const isVideoFeed = useMemo(() => {
79 const isBskyVideoFeed = VIDEO_FEED_URIS.includes(feedInfo.uri)
80 const feedIsVideoMode =
81 feedInfo.contentMode === AppBskyFeedDefs.CONTENTMODEVIDEO
82 const _isVideoFeed = isBskyVideoFeed || feedIsVideoMode
83 return isNative && _isVideoFeed
84 }, [feedInfo])
85
86 useEffect(() => {
87 if (isPageFocused) {
88 setHomeBadge(hasNew)
89 }
90 }, [isPageFocused, hasNew, setHomeBadge])
91
92 const scrollToTop = useCallback(() => {
93 scrollElRef.current?.scrollToOffset({
94 animated: isNative,
95 offset: -headerOffset,
96 })
97 setMinimalShellMode(false)
98 }, [headerOffset, setMinimalShellMode])
99
100 const onSoftReset = useCallback(() => {
101 const isScreenFocused =
102 getTabState(getRootNavigation(navigation).getState(), 'Home') ===
103 TabState.InsideAtRoot
104 if (isScreenFocused && isPageFocused) {
105 scrollToTop()
106 truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
107 setHasNew(false)
108 logEvent('feed:refresh', {
109 feedType: feed.split('|')[0],
110 feedUrl: feed,
111 reason: 'soft-reset',
112 })
113 }
114 }, [navigation, isPageFocused, scrollToTop, queryClient, feed])
115
116 // fires when page within screen is activated/deactivated
117 useEffect(() => {
118 if (!isPageFocused) {
119 return
120 }
121 return listenSoftReset(onSoftReset)
122 }, [onSoftReset, isPageFocused])
123
124 const onPressCompose = useCallback(() => {
125 openComposer({})
126 }, [openComposer])
127
128 const onPressLoadLatest = useCallback(() => {
129 scrollToTop()
130 truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
131 setHasNew(false)
132 logEvent('feed:refresh', {
133 feedType: feed.split('|')[0],
134 feedUrl: feed,
135 reason: 'load-latest',
136 })
137 }, [scrollToTop, feed, queryClient])
138
139 const shouldPrefetch = isNative && isPageAdjacent
140 const isDiscoverFeed = feedInfo.uri === DISCOVER_FEED_URI
141 return (
142 <View
143 testID={testID}
144 // @ts-expect-error web only -sfn
145 dataSet={{nosnippet: isDiscoverFeed ? '' : undefined}}>
146 <MainScrollProvider>
147 <FeedFeedbackProvider value={feedFeedback}>
148 <PostFeed
149 testID={testID ? `${testID}-feed` : undefined}
150 enabled={isPageFocused || shouldPrefetch}
151 feed={feed}
152 feedParams={feedParams}
153 pollInterval={POLL_FREQ}
154 disablePoll={hasNew || !isPageFocused}
155 scrollElRef={scrollElRef}
156 onScrolledDownChange={setIsScrolledDown}
157 onHasNew={setHasNew}
158 renderEmptyState={renderEmptyState}
159 renderEndOfFeed={renderEndOfFeed}
160 headerOffset={headerOffset}
161 savedFeedConfig={savedFeedConfig}
162 isVideoFeed={isVideoFeed}
163 />
164 </FeedFeedbackProvider>
165 </MainScrollProvider>
166 {(isScrolledDown || hasNew) && (
167 <LoadLatestBtn
168 onPress={onPressLoadLatest}
169 label={_(msg`Load new posts`)}
170 showIndicator={hasNew}
171 />
172 )}
173
174 {hasSession && (
175 <FAB
176 testID="composeFAB"
177 onPress={onPressCompose}
178 icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
179 accessibilityRole="button"
180 accessibilityLabel={_(msg({message: `New post`, context: 'action'}))}
181 accessibilityHint=""
182 />
183 )}
184 </View>
185 )
186}