mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React from 'react'
2import {
3 ActivityIndicator,
4 findNodeHandle,
5 ListRenderItemInfo,
6 StyleProp,
7 StyleSheet,
8 View,
9 ViewStyle,
10} from 'react-native'
11import {msg} from '@lingui/macro'
12import {useLingui} from '@lingui/react'
13import {useQueryClient} from '@tanstack/react-query'
14
15import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
16import {cleanError} from '#/lib/strings/errors'
17import {logger} from '#/logger'
18import {isNative, isWeb} from '#/platform/detection'
19import {RQKEY, useProfileListsQuery} from '#/state/queries/profile-lists'
20import {EmptyState} from '#/view/com/util/EmptyState'
21import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
22import {atoms as a, ios, useTheme} from '#/alf'
23import * as ListCard from '#/components/ListCard'
24import {ErrorMessage} from '../util/error/ErrorMessage'
25import {List, ListRef} from '../util/List'
26import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
27
28const LOADING = {_reactKey: '__loading__'}
29const EMPTY = {_reactKey: '__empty__'}
30const ERROR_ITEM = {_reactKey: '__error__'}
31const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
32
33interface SectionRef {
34 scrollToTop: () => void
35}
36
37interface ProfileListsProps {
38 did: string
39 scrollElRef: ListRef
40 headerOffset: number
41 enabled?: boolean
42 style?: StyleProp<ViewStyle>
43 testID?: string
44 setScrollViewTag: (tag: number | null) => void
45}
46
47export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
48 function ProfileListsImpl(
49 {did, scrollElRef, headerOffset, enabled, style, testID, setScrollViewTag},
50 ref,
51 ) {
52 const t = useTheme()
53 const {_} = useLingui()
54 const [isPTRing, setIsPTRing] = React.useState(false)
55 const opts = React.useMemo(() => ({enabled}), [enabled])
56 const {
57 data,
58 isFetching,
59 isFetched,
60 hasNextPage,
61 fetchNextPage,
62 isFetchingNextPage,
63 isError,
64 error,
65 refetch,
66 } = useProfileListsQuery(did, opts)
67 const {isMobile} = useWebMediaQueries()
68 const isEmpty = !isFetching && !data?.pages[0]?.lists.length
69
70 const items = React.useMemo(() => {
71 let items: any[] = []
72 if (isError && isEmpty) {
73 items = items.concat([ERROR_ITEM])
74 }
75 if (!isFetched && isFetching) {
76 items = items.concat([LOADING])
77 } else if (isEmpty) {
78 items = items.concat([EMPTY])
79 } else if (data?.pages) {
80 for (const page of data?.pages) {
81 items = items.concat(page.lists)
82 }
83 }
84 if (isError && !isEmpty) {
85 items = items.concat([LOAD_MORE_ERROR_ITEM])
86 }
87 return items
88 }, [isError, isEmpty, isFetched, isFetching, data])
89
90 // events
91 // =
92
93 const queryClient = useQueryClient()
94
95 const onScrollToTop = React.useCallback(() => {
96 scrollElRef.current?.scrollToOffset({
97 animated: isNative,
98 offset: -headerOffset,
99 })
100 queryClient.invalidateQueries({queryKey: RQKEY(did)})
101 }, [scrollElRef, queryClient, headerOffset, did])
102
103 React.useImperativeHandle(ref, () => ({
104 scrollToTop: onScrollToTop,
105 }))
106
107 const onRefresh = React.useCallback(async () => {
108 setIsPTRing(true)
109 try {
110 await refetch()
111 } catch (err) {
112 logger.error('Failed to refresh lists', {message: err})
113 }
114 setIsPTRing(false)
115 }, [refetch, setIsPTRing])
116
117 const onEndReached = React.useCallback(async () => {
118 if (isFetching || !hasNextPage || isError) return
119 try {
120 await fetchNextPage()
121 } catch (err) {
122 logger.error('Failed to load more lists', {message: err})
123 }
124 }, [isFetching, hasNextPage, isError, fetchNextPage])
125
126 const onPressRetryLoadMore = React.useCallback(() => {
127 fetchNextPage()
128 }, [fetchNextPage])
129
130 // rendering
131 // =
132
133 const renderItemInner = React.useCallback(
134 ({item, index}: ListRenderItemInfo<any>) => {
135 if (item === EMPTY) {
136 return (
137 <EmptyState
138 icon="list-ul"
139 message={_(msg`You have no lists.`)}
140 testID="listsEmpty"
141 />
142 )
143 } else if (item === ERROR_ITEM) {
144 return (
145 <ErrorMessage
146 message={cleanError(error)}
147 onPressTryAgain={refetch}
148 />
149 )
150 } else if (item === LOAD_MORE_ERROR_ITEM) {
151 return (
152 <LoadMoreRetryBtn
153 label={_(
154 msg`There was an issue fetching your lists. Tap here to try again.`,
155 )}
156 onPress={onPressRetryLoadMore}
157 />
158 )
159 } else if (item === LOADING) {
160 return <FeedLoadingPlaceholder />
161 }
162 return (
163 <View
164 style={[
165 (index !== 0 || isWeb) && a.border_t,
166 t.atoms.border_contrast_low,
167 a.px_lg,
168 a.py_lg,
169 ]}>
170 <ListCard.Default view={item} />
171 </View>
172 )
173 },
174 [error, refetch, onPressRetryLoadMore, _, t.atoms.border_contrast_low],
175 )
176
177 React.useEffect(() => {
178 if (enabled && scrollElRef.current) {
179 const nativeTag = findNodeHandle(scrollElRef.current)
180 setScrollViewTag(nativeTag)
181 }
182 }, [enabled, scrollElRef, setScrollViewTag])
183
184 const ProfileListsFooter = React.useCallback(() => {
185 return isFetchingNextPage ? (
186 <ActivityIndicator style={[styles.footer]} />
187 ) : null
188 }, [isFetchingNextPage])
189
190 return (
191 <View testID={testID} style={style}>
192 <List
193 testID={testID ? `${testID}-flatlist` : undefined}
194 ref={scrollElRef}
195 data={items}
196 keyExtractor={(item: any) => item._reactKey || item.uri}
197 renderItem={renderItemInner}
198 ListFooterComponent={ProfileListsFooter}
199 refreshing={isPTRing}
200 onRefresh={onRefresh}
201 headerOffset={headerOffset}
202 progressViewOffset={ios(0)}
203 contentContainerStyle={
204 isMobile && {paddingBottom: headerOffset + 100}
205 }
206 indicatorStyle={t.name === 'light' ? 'black' : 'white'}
207 removeClippedSubviews={true}
208 // @ts-ignore our .web version only -prf
209 desktopFixedHeight
210 onEndReached={onEndReached}
211 />
212 </View>
213 )
214 },
215)
216
217const styles = StyleSheet.create({
218 footer: {paddingTop: 20},
219})