forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {
2 useCallback,
3 useEffect,
4 useImperativeHandle,
5 useMemo,
6 useState,
7} from 'react'
8import {
9 findNodeHandle,
10 type ListRenderItemInfo,
11 type StyleProp,
12 useWindowDimensions,
13 View,
14 type ViewStyle,
15} from 'react-native'
16import {msg} from '@lingui/macro'
17import {useLingui} from '@lingui/react'
18import {useNavigation} from '@react-navigation/native'
19import {useQueryClient} from '@tanstack/react-query'
20
21import {cleanError} from '#/lib/strings/errors'
22import {logger} from '#/logger'
23import {usePreferencesQuery} from '#/state/queries/preferences'
24import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens'
25import {useSession} from '#/state/session'
26import {EmptyState} from '#/view/com/util/EmptyState'
27import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
28import {List, type ListRef} from '#/view/com/util/List'
29import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
30import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
31import {atoms as a, ios, useTheme} from '#/alf'
32import * as FeedCard from '#/components/FeedCard'
33import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag'
34import {ListFooter} from '#/components/Lists'
35import {IS_IOS, IS_NATIVE, IS_WEB} from '#/env'
36
37const LOADING = {_reactKey: '__loading__'}
38const EMPTY = {_reactKey: '__empty__'}
39const ERROR_ITEM = {_reactKey: '__error__'}
40const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
41
42interface SectionRef {
43 scrollToTop: () => void
44}
45
46interface ProfileFeedgensProps {
47 ref?: React.Ref<SectionRef>
48 did: string
49 scrollElRef: ListRef
50 headerOffset: number
51 enabled?: boolean
52 style?: StyleProp<ViewStyle>
53 testID?: string
54 setScrollViewTag: (tag: number | null) => void
55}
56
57export function ProfileFeedgens({
58 ref,
59 did,
60 scrollElRef,
61 headerOffset,
62 enabled,
63 style,
64 testID,
65 setScrollViewTag,
66}: ProfileFeedgensProps) {
67 const {_} = useLingui()
68 const t = useTheme()
69 const [isPTRing, setIsPTRing] = useState(false)
70 const {height} = useWindowDimensions()
71 const opts = useMemo(() => ({enabled}), [enabled])
72 const {
73 data,
74 isPending,
75 isFetchingNextPage,
76 hasNextPage,
77 fetchNextPage,
78 isError,
79 error,
80 refetch,
81 } = useProfileFeedgensQuery(did, opts)
82 const isEmpty = !isPending && !data?.pages[0]?.feeds.length
83 const {data: preferences} = usePreferencesQuery()
84 const navigation = useNavigation()
85 const {currentAccount} = useSession()
86 const isSelf = currentAccount?.did === did
87
88 const items = useMemo(() => {
89 let items: any[] = []
90 if (isError && isEmpty) {
91 items = items.concat([ERROR_ITEM])
92 }
93 if (isPending) {
94 items = items.concat([LOADING])
95 } else if (isEmpty) {
96 items = items.concat([EMPTY])
97 } else if (data?.pages) {
98 for (const page of data?.pages) {
99 items = items.concat(page.feeds)
100 }
101 } else if (isError && !isEmpty) {
102 items = items.concat([LOAD_MORE_ERROR_ITEM])
103 }
104 return items
105 }, [isError, isEmpty, isPending, data])
106
107 // events
108 // =
109
110 const queryClient = useQueryClient()
111
112 const onScrollToTop = useCallback(() => {
113 scrollElRef.current?.scrollToOffset({
114 animated: IS_NATIVE,
115 offset: -headerOffset,
116 })
117 queryClient.invalidateQueries({queryKey: RQKEY(did)})
118 }, [scrollElRef, queryClient, headerOffset, did])
119
120 useImperativeHandle(ref, () => ({
121 scrollToTop: onScrollToTop,
122 }))
123
124 const onRefresh = useCallback(async () => {
125 setIsPTRing(true)
126 try {
127 await refetch()
128 } catch (err) {
129 logger.error('Failed to refresh feeds', {message: err})
130 }
131 setIsPTRing(false)
132 }, [refetch, setIsPTRing])
133
134 const onEndReached = useCallback(async () => {
135 if (isFetchingNextPage || !hasNextPage || isError) return
136
137 try {
138 await fetchNextPage()
139 } catch (err) {
140 logger.error('Failed to load more feeds', {message: err})
141 }
142 }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
143
144 const onPressRetryLoadMore = useCallback(() => {
145 fetchNextPage()
146 }, [fetchNextPage])
147
148 // rendering
149 // =
150
151 const renderItem = useCallback(
152 ({item, index}: ListRenderItemInfo<any>) => {
153 if (item === EMPTY) {
154 return (
155 <EmptyState
156 style={{width: '100%'}}
157 icon={HashtagWideIcon}
158 message={
159 isSelf
160 ? _(msg`You haven't made any custom feeds yet.`)
161 : _(msg`No custom feeds yet`)
162 }
163 textStyle={[t.atoms.text_contrast_medium, a.font_medium]}
164 button={
165 isSelf
166 ? {
167 label: _(msg`Browse custom feeds`),
168 text: _(msg`Browse custom feeds`),
169 onPress: () => navigation.navigate('Feeds' as never),
170 size: 'small',
171 color: 'secondary',
172 }
173 : undefined
174 }
175 />
176 )
177 } else if (item === ERROR_ITEM) {
178 return (
179 <ErrorMessage message={cleanError(error)} onPressTryAgain={refetch} />
180 )
181 } else if (item === LOAD_MORE_ERROR_ITEM) {
182 return (
183 <LoadMoreRetryBtn
184 label={_(
185 msg`There was an issue fetching your lists. Tap here to try again.`,
186 )}
187 onPress={onPressRetryLoadMore}
188 />
189 )
190 } else if (item === LOADING) {
191 return <FeedLoadingPlaceholder />
192 }
193 if (preferences) {
194 return (
195 <View
196 style={[
197 (index !== 0 || IS_WEB) && a.border_t,
198 t.atoms.border_contrast_low,
199 a.px_lg,
200 a.py_lg,
201 ]}>
202 <FeedCard.Default view={item} />
203 </View>
204 )
205 }
206 return null
207 },
208 [
209 _,
210 t,
211 error,
212 refetch,
213 onPressRetryLoadMore,
214 preferences,
215 navigation,
216 isSelf,
217 ],
218 )
219
220 useEffect(() => {
221 if (IS_IOS && enabled && scrollElRef.current) {
222 const nativeTag = findNodeHandle(scrollElRef.current)
223 setScrollViewTag(nativeTag)
224 }
225 }, [enabled, scrollElRef, setScrollViewTag])
226
227 const ProfileFeedgensFooter = useCallback(() => {
228 if (isEmpty) return null
229 return (
230 <ListFooter
231 hasNextPage={hasNextPage}
232 isFetchingNextPage={isFetchingNextPage}
233 onRetry={fetchNextPage}
234 error={cleanError(error)}
235 height={180 + headerOffset}
236 />
237 )
238 }, [
239 hasNextPage,
240 error,
241 isFetchingNextPage,
242 headerOffset,
243 fetchNextPage,
244 isEmpty,
245 ])
246
247 return (
248 <View testID={testID} style={style}>
249 <List
250 testID={testID ? `${testID}-flatlist` : undefined}
251 ref={scrollElRef}
252 data={items}
253 keyExtractor={keyExtractor}
254 renderItem={renderItem}
255 ListFooterComponent={ProfileFeedgensFooter}
256 refreshing={isPTRing}
257 onRefresh={onRefresh}
258 headerOffset={headerOffset}
259 progressViewOffset={ios(0)}
260 removeClippedSubviews={true}
261 desktopFixedHeight
262 onEndReached={onEndReached}
263 contentContainerStyle={{minHeight: height + headerOffset}}
264 />
265 </View>
266 )
267}
268
269function keyExtractor(item: any) {
270 return item._reactKey || item.uri
271}