mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React from 'react'
2import {
3 FontAwesomeIcon,
4 FontAwesomeIconStyle,
5} from '@fortawesome/react-native-fontawesome'
6import {useNavigation} from '@react-navigation/native'
7import {useAnalytics} from 'lib/analytics/analytics'
8import {useQueryClient} from '@tanstack/react-query'
9import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
10import {MainScrollProvider} from '../util/MainScrollProvider'
11import {usePalette} from 'lib/hooks/usePalette'
12import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
13import {useSetMinimalShellMode} from '#/state/shell'
14import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
15import {ComposeIcon2} from 'lib/icons'
16import {colors, s} from 'lib/styles'
17import {View, useWindowDimensions} from 'react-native'
18import {ListMethods} from '../util/List'
19import {Feed} from '../posts/Feed'
20import {TextLink} from '../util/Link'
21import {FAB} from '../util/fab/FAB'
22import {LoadLatestBtn} from '../util/load-latest/LoadLatestBtn'
23import {msg} from '@lingui/macro'
24import {useLingui} from '@lingui/react'
25import {useSession} from '#/state/session'
26import {useComposerControls} from '#/state/shell/composer'
27import {listenSoftReset, emitSoftReset} from '#/state/events'
28import {truncateAndInvalidate} from '#/state/queries/util'
29import {TabState, getTabState, getRootNavigation} from '#/lib/routes/helpers'
30import {isNative} from '#/platform/detection'
31
32const POLL_FREQ = 60e3 // 60sec
33
34export function FeedPage({
35 testID,
36 isPageFocused,
37 feed,
38 feedParams,
39 renderEmptyState,
40 renderEndOfFeed,
41}: {
42 testID?: string
43 feed: FeedDescriptor
44 feedParams?: FeedParams
45 isPageFocused: boolean
46 renderEmptyState: () => JSX.Element
47 renderEndOfFeed?: () => JSX.Element
48}) {
49 const {isSandbox, hasSession} = useSession()
50 const pal = usePalette('default')
51 const {_} = useLingui()
52 const navigation = useNavigation()
53 const {isDesktop} = useWebMediaQueries()
54 const queryClient = useQueryClient()
55 const {openComposer} = useComposerControls()
56 const [isScrolledDown, setIsScrolledDown] = React.useState(false)
57 const setMinimalShellMode = useSetMinimalShellMode()
58 const {screen, track} = useAnalytics()
59 const headerOffset = useHeaderOffset()
60 const scrollElRef = React.useRef<ListMethods>(null)
61 const [hasNew, setHasNew] = React.useState(false)
62
63 const scrollToTop = React.useCallback(() => {
64 scrollElRef.current?.scrollToOffset({
65 animated: isNative,
66 offset: -headerOffset,
67 })
68 setMinimalShellMode(false)
69 }, [headerOffset, setMinimalShellMode])
70
71 const onSoftReset = React.useCallback(() => {
72 const isScreenFocused =
73 getTabState(getRootNavigation(navigation).getState(), 'Home') ===
74 TabState.InsideAtRoot
75 if (isScreenFocused && isPageFocused) {
76 scrollToTop()
77 truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
78 setHasNew(false)
79 }
80 }, [navigation, isPageFocused, scrollToTop, queryClient, feed, setHasNew])
81
82 // fires when page within screen is activated/deactivated
83 React.useEffect(() => {
84 if (!isPageFocused) {
85 return
86 }
87 screen('Feed')
88 return listenSoftReset(onSoftReset)
89 }, [onSoftReset, screen, isPageFocused])
90
91 const onPressCompose = React.useCallback(() => {
92 track('HomeScreen:PressCompose')
93 openComposer({})
94 }, [openComposer, track])
95
96 const onPressLoadLatest = React.useCallback(() => {
97 scrollToTop()
98 truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
99 setHasNew(false)
100 }, [scrollToTop, feed, queryClient, setHasNew])
101
102 const ListHeaderComponent = React.useCallback(() => {
103 if (isDesktop) {
104 return (
105 <View
106 style={[
107 pal.view,
108 {
109 flexDirection: 'row',
110 alignItems: 'center',
111 justifyContent: 'space-between',
112 paddingHorizontal: 18,
113 paddingVertical: 12,
114 },
115 ]}>
116 <TextLink
117 type="title-lg"
118 href="/"
119 style={[pal.text, {fontWeight: 'bold'}]}
120 text={
121 <>
122 {isSandbox ? 'SANDBOX' : 'Bluesky'}{' '}
123 {hasNew && (
124 <View
125 style={{
126 top: -8,
127 backgroundColor: colors.blue3,
128 width: 8,
129 height: 8,
130 borderRadius: 4,
131 }}
132 />
133 )}
134 </>
135 }
136 onPress={emitSoftReset}
137 />
138 {hasSession && (
139 <TextLink
140 type="title-lg"
141 href="/settings/home-feed"
142 style={{fontWeight: 'bold'}}
143 accessibilityLabel={_(msg`Feed Preferences`)}
144 accessibilityHint=""
145 text={
146 <FontAwesomeIcon
147 icon="sliders"
148 style={pal.textLight as FontAwesomeIconStyle}
149 />
150 }
151 />
152 )}
153 </View>
154 )
155 }
156 return <></>
157 }, [
158 isDesktop,
159 pal.view,
160 pal.text,
161 pal.textLight,
162 hasNew,
163 _,
164 isSandbox,
165 hasSession,
166 ])
167
168 return (
169 <View testID={testID} style={s.h100pct}>
170 <MainScrollProvider>
171 <Feed
172 testID={testID ? `${testID}-feed` : undefined}
173 enabled={isPageFocused}
174 feed={feed}
175 feedParams={feedParams}
176 pollInterval={POLL_FREQ}
177 scrollElRef={scrollElRef}
178 onScrolledDownChange={setIsScrolledDown}
179 onHasNew={setHasNew}
180 renderEmptyState={renderEmptyState}
181 renderEndOfFeed={renderEndOfFeed}
182 ListHeaderComponent={ListHeaderComponent}
183 headerOffset={headerOffset}
184 />
185 </MainScrollProvider>
186 {(isScrolledDown || hasNew) && (
187 <LoadLatestBtn
188 onPress={onPressLoadLatest}
189 label={_(msg`Load new posts`)}
190 showIndicator={hasNew}
191 />
192 )}
193
194 {hasSession && (
195 <FAB
196 testID="composeFAB"
197 onPress={onPressCompose}
198 icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
199 accessibilityRole="button"
200 accessibilityLabel={_(msg`New post`)}
201 accessibilityHint=""
202 />
203 )}
204 </View>
205 )
206}
207
208function useHeaderOffset() {
209 const {isDesktop, isTablet} = useWebMediaQueries()
210 const {fontScale} = useWindowDimensions()
211 const {hasSession} = useSession()
212
213 if (isDesktop) {
214 return 0
215 }
216 if (isTablet) {
217 if (hasSession) {
218 return 50
219 } else {
220 return 0
221 }
222 }
223
224 if (hasSession) {
225 const navBarPad = 16
226 const navBarText = 21 * fontScale
227 const tabBarPad = 20 + 3 // nav bar padding + border
228 const tabBarText = 16 * fontScale
229 const magic = 7 * fontScale
230 return navBarPad + navBarText + tabBarPad + tabBarText + magic
231 } else {
232 const navBarPad = 16
233 const navBarText = 21 * fontScale
234 const magic = 4 * fontScale
235 return navBarPad + navBarText + magic
236 }
237}