mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React, {MutableRefObject} from 'react'
2import {observer} from 'mobx-react-lite'
3import {
4 ActivityIndicator,
5 RefreshControl,
6 StyleProp,
7 StyleSheet,
8 View,
9 ViewStyle,
10} from 'react-native'
11import {FlatList} from '../util/Views'
12import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
13import {ErrorMessage} from '../util/error/ErrorMessage'
14import {PostsFeedModel} from 'state/models/feeds/posts'
15import {FeedSlice} from './FeedSlice'
16import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
17import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
18import {s} from 'lib/styles'
19import {useAnalytics} from 'lib/analytics/analytics'
20import {usePalette} from 'lib/hooks/usePalette'
21import {useTheme} from 'lib/ThemeContext'
22
23const LOADING_ITEM = {_reactKey: '__loading__'}
24const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
25const ERROR_ITEM = {_reactKey: '__error__'}
26const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
27
28export const Feed = observer(function Feed({
29 feed,
30 style,
31 scrollElRef,
32 onPressTryAgain,
33 onScroll,
34 scrollEventThrottle,
35 renderEmptyState,
36 renderEndOfFeed,
37 testID,
38 headerOffset = 0,
39 ListHeaderComponent,
40 extraData,
41}: {
42 feed: PostsFeedModel
43 style?: StyleProp<ViewStyle>
44 scrollElRef?: MutableRefObject<FlatList<any> | null>
45 onPressTryAgain?: () => void
46 onScroll?: OnScrollCb
47 scrollEventThrottle?: number
48 renderEmptyState: () => JSX.Element
49 renderEndOfFeed?: () => JSX.Element
50 testID?: string
51 headerOffset?: number
52 ListHeaderComponent?: () => JSX.Element
53 extraData?: any
54}) {
55 const pal = usePalette('default')
56 const theme = useTheme()
57 const {track} = useAnalytics()
58 const [isRefreshing, setIsRefreshing] = React.useState(false)
59
60 const data = React.useMemo(() => {
61 let feedItems: any[] = []
62 if (feed.hasLoaded) {
63 if (feed.hasError) {
64 feedItems = feedItems.concat([ERROR_ITEM])
65 }
66 if (feed.isEmpty) {
67 feedItems = feedItems.concat([EMPTY_FEED_ITEM])
68 } else {
69 feedItems = feedItems.concat(feed.slices)
70 }
71 if (feed.loadMoreError) {
72 feedItems = feedItems.concat([LOAD_MORE_ERROR_ITEM])
73 }
74 }
75 return feedItems
76 }, [
77 feed.hasError,
78 feed.hasLoaded,
79 feed.isEmpty,
80 feed.slices,
81 feed.loadMoreError,
82 ])
83
84 // events
85 // =
86
87 const onRefresh = React.useCallback(async () => {
88 track('Feed:onRefresh')
89 setIsRefreshing(true)
90 try {
91 await feed.refresh()
92 } catch (err) {
93 feed.rootStore.log.error('Failed to refresh posts feed', err)
94 }
95 setIsRefreshing(false)
96 }, [feed, track, setIsRefreshing])
97
98 const onEndReached = React.useCallback(async () => {
99 if (!feed.hasLoaded || !feed.hasMore) return
100
101 track('Feed:onEndReached')
102 try {
103 await feed.loadMore()
104 } catch (err) {
105 feed.rootStore.log.error('Failed to load more posts', err)
106 }
107 }, [feed, track])
108
109 const onPressRetryLoadMore = React.useCallback(() => {
110 feed.retryLoadMore()
111 }, [feed])
112
113 // rendering
114 // =
115
116 const renderItem = React.useCallback(
117 ({item}: {item: any}) => {
118 if (item === EMPTY_FEED_ITEM) {
119 return renderEmptyState()
120 } else if (item === ERROR_ITEM) {
121 return (
122 <ErrorMessage
123 message={feed.error}
124 onPressTryAgain={onPressTryAgain}
125 />
126 )
127 } else if (item === LOAD_MORE_ERROR_ITEM) {
128 return (
129 <LoadMoreRetryBtn
130 label="There was an issue fetching posts. Tap here to try again."
131 onPress={onPressRetryLoadMore}
132 />
133 )
134 } else if (item === LOADING_ITEM) {
135 return <PostFeedLoadingPlaceholder />
136 }
137 return <FeedSlice slice={item} />
138 },
139 [feed, onPressTryAgain, onPressRetryLoadMore, renderEmptyState],
140 )
141
142 const FeedFooter = React.useCallback(
143 () =>
144 feed.isLoadingMore ? (
145 <View style={styles.feedFooter}>
146 <ActivityIndicator />
147 </View>
148 ) : !feed.hasMore && !feed.isEmpty && renderEndOfFeed ? (
149 renderEndOfFeed()
150 ) : (
151 <View />
152 ),
153 [feed.isLoadingMore, feed.hasMore, feed.isEmpty, renderEndOfFeed],
154 )
155
156 return (
157 <View testID={testID} style={style}>
158 <FlatList
159 testID={testID ? `${testID}-flatlist` : undefined}
160 ref={scrollElRef}
161 data={!feed.hasLoaded ? [LOADING_ITEM] : data}
162 keyExtractor={item => item._reactKey}
163 renderItem={renderItem}
164 ListFooterComponent={FeedFooter}
165 ListHeaderComponent={ListHeaderComponent}
166 refreshControl={
167 <RefreshControl
168 refreshing={isRefreshing}
169 onRefresh={onRefresh}
170 tintColor={pal.colors.text}
171 titleColor={pal.colors.text}
172 progressViewOffset={headerOffset}
173 />
174 }
175 contentContainerStyle={s.contentContainer}
176 style={{paddingTop: headerOffset}}
177 onScroll={onScroll}
178 scrollEventThrottle={scrollEventThrottle}
179 indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
180 onEndReached={onEndReached}
181 onEndReachedThreshold={2}
182 removeClippedSubviews={true}
183 contentOffset={{x: 0, y: headerOffset * -1}}
184 extraData={extraData}
185 // @ts-ignore our .web version only -prf
186 desktopFixedHeight
187 />
188 </View>
189 )
190})
191
192const styles = StyleSheet.create({
193 feedFooter: {paddingTop: 20},
194})