diff options
Diffstat (limited to 'src/components/StarterPack/ProfileStarterPacks.tsx')
-rw-r--r-- | src/components/StarterPack/ProfileStarterPacks.tsx | 320 |
1 files changed, 320 insertions, 0 deletions
diff --git a/src/components/StarterPack/ProfileStarterPacks.tsx b/src/components/StarterPack/ProfileStarterPacks.tsx new file mode 100644 index 000000000..096f04f2d --- /dev/null +++ b/src/components/StarterPack/ProfileStarterPacks.tsx @@ -0,0 +1,320 @@ +import React from 'react' +import { + findNodeHandle, + ListRenderItemInfo, + StyleProp, + View, + ViewStyle, +} from 'react-native' +import {AppBskyGraphDefs, AppBskyGraphGetActorStarterPacks} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useNavigation} from '@react-navigation/native' +import {InfiniteData, UseInfiniteQueryResult} from '@tanstack/react-query' + +import {logger} from '#/logger' +import {useGenerateStarterPackMutation} from 'lib/generate-starterpack' +import {useBottomBarOffset} from 'lib/hooks/useBottomBarOffset' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {NavigationProp} from 'lib/routes/types' +import {parseStarterPackUri} from 'lib/strings/starter-pack' +import {List, ListRef} from 'view/com/util/List' +import {Text} from 'view/com/util/text/Text' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {useDialogControl} from '#/components/Dialog' +import {LinearGradientBackground} from '#/components/LinearGradientBackground' +import {Loader} from '#/components/Loader' +import * as Prompt from '#/components/Prompt' +import {Default as StarterPackCard} from '#/components/StarterPack/StarterPackCard' +import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '../icons/Plus' + +interface SectionRef { + scrollToTop: () => void +} + +interface ProfileFeedgensProps { + starterPacksQuery: UseInfiniteQueryResult< + InfiniteData<AppBskyGraphGetActorStarterPacks.OutputSchema, unknown>, + Error + > + scrollElRef: ListRef + headerOffset: number + enabled?: boolean + style?: StyleProp<ViewStyle> + testID?: string + setScrollViewTag: (tag: number | null) => void + isMe: boolean +} + +function keyExtractor(item: AppBskyGraphDefs.StarterPackView) { + return item.uri +} + +export const ProfileStarterPacks = React.forwardRef< + SectionRef, + ProfileFeedgensProps +>(function ProfileFeedgensImpl( + { + starterPacksQuery: query, + scrollElRef, + headerOffset, + enabled, + style, + testID, + setScrollViewTag, + isMe, + }, + ref, +) { + const t = useTheme() + const bottomBarOffset = useBottomBarOffset(100) + const [isPTRing, setIsPTRing] = React.useState(false) + const {data, refetch, isFetching, hasNextPage, fetchNextPage} = query + const {isTabletOrDesktop} = useWebMediaQueries() + + const items = data?.pages.flatMap(page => page.starterPacks) + + React.useImperativeHandle(ref, () => ({ + scrollToTop: () => {}, + })) + + const onRefresh = React.useCallback(async () => { + setIsPTRing(true) + try { + await refetch() + } catch (err) { + logger.error('Failed to refresh starter packs', {message: err}) + } + setIsPTRing(false) + }, [refetch, setIsPTRing]) + + const onEndReached = React.useCallback(async () => { + if (isFetching || !hasNextPage) return + + try { + await fetchNextPage() + } catch (err) { + logger.error('Failed to load more starter packs', {message: err}) + } + }, [isFetching, hasNextPage, fetchNextPage]) + + React.useEffect(() => { + if (enabled && scrollElRef.current) { + const nativeTag = findNodeHandle(scrollElRef.current) + setScrollViewTag(nativeTag) + } + }, [enabled, scrollElRef, setScrollViewTag]) + + const renderItem = ({ + item, + index, + }: ListRenderItemInfo<AppBskyGraphDefs.StarterPackView>) => { + return ( + <View + style={[ + a.p_lg, + (isTabletOrDesktop || index !== 0) && a.border_t, + t.atoms.border_contrast_low, + ]}> + <StarterPackCard starterPack={item} /> + </View> + ) + } + + return ( + <View testID={testID} style={style}> + <List + testID={testID ? `${testID}-flatlist` : undefined} + ref={scrollElRef} + data={items} + renderItem={renderItem} + keyExtractor={keyExtractor} + refreshing={isPTRing} + headerOffset={headerOffset} + contentContainerStyle={{paddingBottom: headerOffset + bottomBarOffset}} + indicatorStyle={t.name === 'light' ? 'black' : 'white'} + removeClippedSubviews={true} + desktopFixedHeight + onEndReached={onEndReached} + onRefresh={onRefresh} + ListEmptyComponent={Empty} + ListFooterComponent={ + items?.length !== 0 && isMe ? CreateAnother : undefined + } + /> + </View> + ) +}) + +function CreateAnother() { + const {_} = useLingui() + const t = useTheme() + const navigation = useNavigation<NavigationProp>() + + return ( + <View + style={[ + a.pr_md, + a.pt_lg, + a.gap_lg, + a.border_t, + t.atoms.border_contrast_low, + ]}> + <Button + label={_(msg`Create a starter pack`)} + variant="solid" + color="secondary" + size="small" + style={[a.self_center]} + onPress={() => navigation.navigate('StarterPackWizard')}> + <ButtonText> + <Trans>Create another</Trans> + </ButtonText> + <ButtonIcon icon={Plus} position="right" /> + </Button> + </View> + ) +} + +function Empty() { + const {_} = useLingui() + const t = useTheme() + const navigation = useNavigation<NavigationProp>() + const confirmDialogControl = useDialogControl() + const followersDialogControl = useDialogControl() + const errorDialogControl = useDialogControl() + + const [isGenerating, setIsGenerating] = React.useState(false) + + const {mutate: generateStarterPack} = useGenerateStarterPackMutation({ + onSuccess: ({uri}) => { + const parsed = parseStarterPackUri(uri) + if (parsed) { + navigation.push('StarterPack', { + name: parsed.name, + rkey: parsed.rkey, + }) + } + setIsGenerating(false) + }, + onError: e => { + logger.error('Failed to generate starter pack', {safeMessage: e}) + setIsGenerating(false) + if (e.name === 'NOT_ENOUGH_FOLLOWERS') { + followersDialogControl.open() + } else { + errorDialogControl.open() + } + }, + }) + + const generate = () => { + setIsGenerating(true) + generateStarterPack() + } + + return ( + <LinearGradientBackground + style={[ + a.px_lg, + a.py_lg, + a.justify_between, + a.gap_lg, + a.shadow_lg, + {marginTop: 2}, + ]}> + <View style={[a.gap_xs]}> + <Text + style={[ + a.font_bold, + a.text_lg, + t.atoms.text_contrast_medium, + {color: 'white'}, + ]}> + You haven't created a starter pack yet! + </Text> + <Text style={[a.text_md, {color: 'white'}]}> + Starter packs let you easily share your favorite feeds and people with + your friends. + </Text> + </View> + <View style={[a.flex_row, a.gap_md, {marginLeft: 'auto'}]}> + <Button + label={_(msg`Create a starter pack for me`)} + variant="ghost" + color="primary" + size="small" + disabled={isGenerating} + onPress={confirmDialogControl.open} + style={{backgroundColor: 'transparent'}}> + <ButtonText style={{color: 'white'}}> + <Trans>Make one for me</Trans> + </ButtonText> + {isGenerating && <Loader size="md" />} + </Button> + <Button + label={_(msg`Create a starter pack`)} + variant="ghost" + color="primary" + size="small" + disabled={isGenerating} + onPress={() => navigation.navigate('StarterPackWizard')} + style={{ + backgroundColor: 'white', + borderColor: 'white', + width: 100, + }} + hoverStyle={[{backgroundColor: '#dfdfdf'}]}> + <ButtonText> + <Trans>Create</Trans> + </ButtonText> + </Button> + </View> + + <Prompt.Outer control={confirmDialogControl}> + <Prompt.TitleText> + <Trans>Generate a starter pack</Trans> + </Prompt.TitleText> + <Prompt.DescriptionText> + <Trans> + Bluesky will choose a set of recommended accounts from people in + your network. + </Trans> + </Prompt.DescriptionText> + <Prompt.Actions> + <Prompt.Action + color="primary" + cta={_(msg`Choose for me`)} + onPress={generate} + /> + <Prompt.Action + color="secondary" + cta={_(msg`Let me choose`)} + onPress={() => { + navigation.navigate('StarterPackWizard') + }} + /> + </Prompt.Actions> + </Prompt.Outer> + <Prompt.Basic + control={followersDialogControl} + title={_(msg`Oops!`)} + description={_( + msg`You must be following at least seven other people to generate a starter pack.`, + )} + onConfirm={() => {}} + showCancel={false} + /> + <Prompt.Basic + control={errorDialogControl} + title={_(msg`Oops!`)} + description={_( + msg`An error occurred while generating your starter pack. Want to try again?`, + )} + onConfirm={generate} + confirmButtonCta={_(msg`Retry`)} + /> + </LinearGradientBackground> + ) +} |