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