mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React from 'react'
2import {
3 ActivityIndicator,
4 Dimensions,
5 StyleProp,
6 View,
7 ViewStyle,
8} from 'react-native'
9import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
10import {List, ListRef} from '../util/List'
11import {ProfileCardFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
12import {ErrorMessage} from '../util/error/ErrorMessage'
13import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
14import {ProfileCard} from '../profile/ProfileCard'
15import {Button} from '../util/forms/Button'
16import {useAnalytics} from 'lib/analytics/analytics'
17import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
18import {useListMembersQuery} from '#/state/queries/list-members'
19import {logger} from '#/logger'
20import {useModalControls} from '#/state/modals'
21import {useSession} from '#/state/session'
22import {cleanError} from '#/lib/strings/errors'
23import {useLingui} from '@lingui/react'
24import {msg} from '@lingui/macro'
25
26const LOADING_ITEM = {_reactKey: '__loading__'}
27const EMPTY_ITEM = {_reactKey: '__empty__'}
28const ERROR_ITEM = {_reactKey: '__error__'}
29const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
30
31export function ListMembers({
32 list,
33 style,
34 scrollElRef,
35 onScrolledDownChange,
36 onPressTryAgain,
37 renderHeader,
38 renderEmptyState,
39 testID,
40 headerOffset = 0,
41 desktopFixedHeightOffset,
42}: {
43 list: string
44 style?: StyleProp<ViewStyle>
45 scrollElRef?: ListRef
46 onScrolledDownChange: (isScrolledDown: boolean) => void
47 onPressTryAgain?: () => void
48 renderHeader: () => JSX.Element
49 renderEmptyState: () => JSX.Element
50 testID?: string
51 headerOffset?: number
52 desktopFixedHeightOffset?: number
53}) {
54 const {track} = useAnalytics()
55 const {_} = useLingui()
56 const [isRefreshing, setIsRefreshing] = React.useState(false)
57 const {isMobile} = useWebMediaQueries()
58 const {openModal} = useModalControls()
59 const {currentAccount} = useSession()
60
61 const {
62 data,
63 isFetching,
64 isFetched,
65 isError,
66 error,
67 refetch,
68 fetchNextPage,
69 hasNextPage,
70 } = useListMembersQuery(list)
71 const isEmpty = !isFetching && !data?.pages[0].items.length
72 const isOwner =
73 currentAccount && data?.pages[0].list.creator.did === currentAccount.did
74
75 const items = React.useMemo(() => {
76 let items: any[] = []
77 if (isFetched) {
78 if (isEmpty && isError) {
79 items = items.concat([ERROR_ITEM])
80 }
81 if (isEmpty) {
82 items = items.concat([EMPTY_ITEM])
83 } else if (data) {
84 for (const page of data.pages) {
85 items = items.concat(page.items)
86 }
87 }
88 if (!isEmpty && isError) {
89 items = items.concat([LOAD_MORE_ERROR_ITEM])
90 }
91 } else if (isFetching) {
92 items = items.concat([LOADING_ITEM])
93 }
94 return items
95 }, [isFetched, isEmpty, isError, data, isFetching])
96
97 // events
98 // =
99
100 const onRefresh = React.useCallback(async () => {
101 track('Lists:onRefresh')
102 setIsRefreshing(true)
103 try {
104 await refetch()
105 } catch (err) {
106 logger.error('Failed to refresh lists', {message: err})
107 }
108 setIsRefreshing(false)
109 }, [refetch, track, setIsRefreshing])
110
111 const onEndReached = React.useCallback(async () => {
112 if (isFetching || !hasNextPage || isError) return
113 track('Lists:onEndReached')
114 try {
115 await fetchNextPage()
116 } catch (err) {
117 logger.error('Failed to load more lists', {message: err})
118 }
119 }, [isFetching, hasNextPage, isError, fetchNextPage, track])
120
121 const onPressRetryLoadMore = React.useCallback(() => {
122 fetchNextPage()
123 }, [fetchNextPage])
124
125 const onPressEditMembership = React.useCallback(
126 (profile: AppBskyActorDefs.ProfileViewBasic) => {
127 openModal({
128 name: 'user-add-remove-lists',
129 subject: profile.did,
130 displayName: profile.displayName || profile.handle,
131 handle: profile.handle,
132 })
133 },
134 [openModal],
135 )
136
137 // rendering
138 // =
139
140 const renderMemberButton = React.useCallback(
141 (profile: AppBskyActorDefs.ProfileViewBasic) => {
142 if (!isOwner) {
143 return null
144 }
145 return (
146 <Button
147 testID={`user-${profile.handle}-editBtn`}
148 type="default"
149 label={_(msg({message: 'Edit', context: 'action'}))}
150 onPress={() => onPressEditMembership(profile)}
151 />
152 )
153 },
154 [isOwner, onPressEditMembership, _],
155 )
156
157 const renderItem = React.useCallback(
158 ({item}: {item: any}) => {
159 if (item === EMPTY_ITEM) {
160 return renderEmptyState()
161 } else if (item === ERROR_ITEM) {
162 return (
163 <ErrorMessage
164 message={cleanError(error)}
165 onPressTryAgain={onPressTryAgain}
166 />
167 )
168 } else if (item === LOAD_MORE_ERROR_ITEM) {
169 return (
170 <LoadMoreRetryBtn
171 label={_(
172 msg`There was an issue fetching the list. Tap here to try again.`,
173 )}
174 onPress={onPressRetryLoadMore}
175 />
176 )
177 } else if (item === LOADING_ITEM) {
178 return <ProfileCardFeedLoadingPlaceholder />
179 }
180 return (
181 <ProfileCard
182 testID={`user-${
183 (item as AppBskyGraphDefs.ListItemView).subject.handle
184 }`}
185 profile={(item as AppBskyGraphDefs.ListItemView).subject}
186 renderButton={renderMemberButton}
187 style={{paddingHorizontal: isMobile ? 8 : 14, paddingVertical: 4}}
188 noModFilter
189 />
190 )
191 },
192 [
193 renderMemberButton,
194 renderEmptyState,
195 error,
196 onPressTryAgain,
197 onPressRetryLoadMore,
198 isMobile,
199 _,
200 ],
201 )
202
203 const Footer = React.useCallback(
204 () => (
205 <View style={{paddingTop: 20, paddingBottom: 400}}>
206 {isFetching && <ActivityIndicator />}
207 </View>
208 ),
209 [isFetching],
210 )
211
212 return (
213 <View testID={testID} style={style}>
214 <List
215 testID={testID ? `${testID}-flatlist` : undefined}
216 ref={scrollElRef}
217 data={items}
218 keyExtractor={(item: any) => item.subject?.did || item._reactKey}
219 renderItem={renderItem}
220 ListHeaderComponent={renderHeader}
221 ListFooterComponent={Footer}
222 refreshing={isRefreshing}
223 onRefresh={onRefresh}
224 headerOffset={headerOffset}
225 contentContainerStyle={{
226 minHeight: Dimensions.get('window').height * 1.5,
227 }}
228 onScrolledDownChange={onScrolledDownChange}
229 onEndReached={onEndReached}
230 onEndReachedThreshold={0.6}
231 removeClippedSubviews={true}
232 // @ts-ignore our .web version only -prf
233 desktopFixedHeight={desktopFixedHeightOffset || true}
234 />
235 </View>
236 )
237}