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