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 {AppBskyGraphDefs, AppBskyGraphGetActorStarterPacks} from '@atproto/api'
10import {msg, Trans} from '@lingui/macro'
11import {useLingui} from '@lingui/react'
12import {useNavigation} from '@react-navigation/native'
13import {InfiniteData, UseInfiniteQueryResult} from '@tanstack/react-query'
14
15import {logger} from '#/logger'
16import {useGenerateStarterPackMutation} from 'lib/generate-starterpack'
17import {useBottomBarOffset} from 'lib/hooks/useBottomBarOffset'
18import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
19import {NavigationProp} from 'lib/routes/types'
20import {parseStarterPackUri} from 'lib/strings/starter-pack'
21import {List, ListRef} from 'view/com/util/List'
22import {Text} from 'view/com/util/text/Text'
23import {atoms as a, useTheme} from '#/alf'
24import {Button, ButtonIcon, ButtonText} from '#/components/Button'
25import {useDialogControl} from '#/components/Dialog'
26import {LinearGradientBackground} from '#/components/LinearGradientBackground'
27import {Loader} from '#/components/Loader'
28import * as Prompt from '#/components/Prompt'
29import {Default as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
30import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '../icons/Plus'
31
32interface SectionRef {
33 scrollToTop: () => void
34}
35
36interface ProfileFeedgensProps {
37 starterPacksQuery: UseInfiniteQueryResult<
38 InfiniteData<AppBskyGraphGetActorStarterPacks.OutputSchema, unknown>,
39 Error
40 >
41 scrollElRef: ListRef
42 headerOffset: number
43 enabled?: boolean
44 style?: StyleProp<ViewStyle>
45 testID?: string
46 setScrollViewTag: (tag: number | null) => void
47 isMe: boolean
48}
49
50function keyExtractor(item: AppBskyGraphDefs.StarterPackView) {
51 return item.uri
52}
53
54export const ProfileStarterPacks = React.forwardRef<
55 SectionRef,
56 ProfileFeedgensProps
57>(function ProfileFeedgensImpl(
58 {
59 starterPacksQuery: query,
60 scrollElRef,
61 headerOffset,
62 enabled,
63 style,
64 testID,
65 setScrollViewTag,
66 isMe,
67 },
68 ref,
69) {
70 const t = useTheme()
71 const bottomBarOffset = useBottomBarOffset(100)
72 const [isPTRing, setIsPTRing] = React.useState(false)
73 const {data, refetch, isFetching, hasNextPage, fetchNextPage} = query
74 const {isTabletOrDesktop} = useWebMediaQueries()
75
76 const items = data?.pages.flatMap(page => page.starterPacks)
77
78 React.useImperativeHandle(ref, () => ({
79 scrollToTop: () => {},
80 }))
81
82 const onRefresh = React.useCallback(async () => {
83 setIsPTRing(true)
84 try {
85 await refetch()
86 } catch (err) {
87 logger.error('Failed to refresh starter packs', {message: err})
88 }
89 setIsPTRing(false)
90 }, [refetch, setIsPTRing])
91
92 const onEndReached = React.useCallback(async () => {
93 if (isFetching || !hasNextPage) return
94
95 try {
96 await fetchNextPage()
97 } catch (err) {
98 logger.error('Failed to load more starter packs', {message: err})
99 }
100 }, [isFetching, hasNextPage, fetchNextPage])
101
102 React.useEffect(() => {
103 if (enabled && scrollElRef.current) {
104 const nativeTag = findNodeHandle(scrollElRef.current)
105 setScrollViewTag(nativeTag)
106 }
107 }, [enabled, scrollElRef, setScrollViewTag])
108
109 const renderItem = ({
110 item,
111 index,
112 }: ListRenderItemInfo<AppBskyGraphDefs.StarterPackView>) => {
113 return (
114 <View
115 style={[
116 a.p_lg,
117 (isTabletOrDesktop || index !== 0) && a.border_t,
118 t.atoms.border_contrast_low,
119 ]}>
120 <StarterPackCard starterPack={item} />
121 </View>
122 )
123 }
124
125 return (
126 <View testID={testID} style={style}>
127 <List
128 testID={testID ? `${testID}-flatlist` : undefined}
129 ref={scrollElRef}
130 data={items}
131 renderItem={renderItem}
132 keyExtractor={keyExtractor}
133 refreshing={isPTRing}
134 headerOffset={headerOffset}
135 contentContainerStyle={{paddingBottom: headerOffset + bottomBarOffset}}
136 indicatorStyle={t.name === 'light' ? 'black' : 'white'}
137 removeClippedSubviews={true}
138 desktopFixedHeight
139 onEndReached={onEndReached}
140 onRefresh={onRefresh}
141 ListEmptyComponent={Empty}
142 ListFooterComponent={
143 items?.length !== 0 && isMe ? CreateAnother : undefined
144 }
145 />
146 </View>
147 )
148})
149
150function CreateAnother() {
151 const {_} = useLingui()
152 const t = useTheme()
153 const navigation = useNavigation<NavigationProp>()
154
155 return (
156 <View
157 style={[
158 a.pr_md,
159 a.pt_lg,
160 a.gap_lg,
161 a.border_t,
162 t.atoms.border_contrast_low,
163 ]}>
164 <Button
165 label={_(msg`Create a starter pack`)}
166 variant="solid"
167 color="secondary"
168 size="small"
169 style={[a.self_center]}
170 onPress={() => navigation.navigate('StarterPackWizard')}>
171 <ButtonText>
172 <Trans>Create another</Trans>
173 </ButtonText>
174 <ButtonIcon icon={Plus} position="right" />
175 </Button>
176 </View>
177 )
178}
179
180function Empty() {
181 const {_} = useLingui()
182 const t = useTheme()
183 const navigation = useNavigation<NavigationProp>()
184 const confirmDialogControl = useDialogControl()
185 const followersDialogControl = useDialogControl()
186 const errorDialogControl = useDialogControl()
187
188 const [isGenerating, setIsGenerating] = React.useState(false)
189
190 const {mutate: generateStarterPack} = useGenerateStarterPackMutation({
191 onSuccess: ({uri}) => {
192 const parsed = parseStarterPackUri(uri)
193 if (parsed) {
194 navigation.push('StarterPack', {
195 name: parsed.name,
196 rkey: parsed.rkey,
197 })
198 }
199 setIsGenerating(false)
200 },
201 onError: e => {
202 logger.error('Failed to generate starter pack', {safeMessage: e})
203 setIsGenerating(false)
204 if (e.name === 'NOT_ENOUGH_FOLLOWERS') {
205 followersDialogControl.open()
206 } else {
207 errorDialogControl.open()
208 }
209 },
210 })
211
212 const generate = () => {
213 setIsGenerating(true)
214 generateStarterPack()
215 }
216
217 return (
218 <LinearGradientBackground
219 style={[
220 a.px_lg,
221 a.py_lg,
222 a.justify_between,
223 a.gap_lg,
224 a.shadow_lg,
225 {marginTop: 2},
226 ]}>
227 <View style={[a.gap_xs]}>
228 <Text
229 style={[
230 a.font_bold,
231 a.text_lg,
232 t.atoms.text_contrast_medium,
233 {color: 'white'},
234 ]}>
235 <Trans>You haven't created a starter pack yet!</Trans>
236 </Text>
237 <Text style={[a.text_md, {color: 'white'}]}>
238 <Trans>
239 Starter packs let you easily share your favorite feeds and people
240 with your friends.
241 </Trans>
242 </Text>
243 </View>
244 <View style={[a.flex_row, a.gap_md, {marginLeft: 'auto'}]}>
245 <Button
246 label={_(msg`Create a starter pack for me`)}
247 variant="ghost"
248 color="primary"
249 size="small"
250 disabled={isGenerating}
251 onPress={confirmDialogControl.open}
252 style={{backgroundColor: 'transparent'}}>
253 <ButtonText style={{color: 'white'}}>
254 <Trans>Make one for me</Trans>
255 </ButtonText>
256 {isGenerating && <Loader size="md" />}
257 </Button>
258 <Button
259 label={_(msg`Create a starter pack`)}
260 variant="ghost"
261 color="primary"
262 size="small"
263 disabled={isGenerating}
264 onPress={() => navigation.navigate('StarterPackWizard')}
265 style={{
266 backgroundColor: 'white',
267 borderColor: 'white',
268 width: 100,
269 }}
270 hoverStyle={[{backgroundColor: '#dfdfdf'}]}>
271 <ButtonText>
272 <Trans>Create</Trans>
273 </ButtonText>
274 </Button>
275 </View>
276
277 <Prompt.Outer control={confirmDialogControl}>
278 <Prompt.TitleText>
279 <Trans>Generate a starter pack</Trans>
280 </Prompt.TitleText>
281 <Prompt.DescriptionText>
282 <Trans>
283 Bluesky will choose a set of recommended accounts from people in
284 your network.
285 </Trans>
286 </Prompt.DescriptionText>
287 <Prompt.Actions>
288 <Prompt.Action
289 color="primary"
290 cta={_(msg`Choose for me`)}
291 onPress={generate}
292 />
293 <Prompt.Action
294 color="secondary"
295 cta={_(msg`Let me choose`)}
296 onPress={() => {
297 navigation.navigate('StarterPackWizard')
298 }}
299 />
300 </Prompt.Actions>
301 </Prompt.Outer>
302 <Prompt.Basic
303 control={followersDialogControl}
304 title={_(msg`Oops!`)}
305 description={_(
306 msg`You must be following at least seven other people to generate a starter pack.`,
307 )}
308 onConfirm={() => {}}
309 showCancel={false}
310 />
311 <Prompt.Basic
312 control={errorDialogControl}
313 title={_(msg`Oops!`)}
314 description={_(
315 msg`An error occurred while generating your starter pack. Want to try again?`,
316 )}
317 onConfirm={generate}
318 confirmButtonCta={_(msg`Retry`)}
319 />
320 </LinearGradientBackground>
321 )
322}