import {useCallback, useState} from 'react' import {View} from 'react-native' import { type AppBskyGraphGetStarterPacksWithMembership, AppBskyGraphStarterpack, } from '@atproto/api' import {msg, Plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification' import {type NavigationProp} from '#/lib/routes/types' import {isWeb} from '#/platform/detection' import { invalidateActorStarterPacksWithMembershipQuery, useActorStarterPacksWithMembershipsQuery, } from '#/state/queries/actor-starter-packs' import { useListMembershipAddMutation, useListMembershipRemoveMutation, } from '#/state/queries/list-memberships' import * as Toast from '#/view/com/util/Toast' import {atoms as a, useTheme} from '#/alf' import {AvatarStack} from '#/components/AvatarStack' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {Divider} from '#/components/Divider' import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' import {StarterPack} from '#/components/icons/StarterPack' import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import * as bsky from '#/types/bsky' type StarterPackWithMembership = AppBskyGraphGetStarterPacksWithMembership.StarterPackWithMembership export type StarterPackDialogProps = { control: Dialog.DialogControlProps targetDid: string enabled?: boolean } export function StarterPackDialog({ control, targetDid, enabled, }: StarterPackDialogProps) { const {_} = useLingui() const navigation = useNavigation() const requireEmailVerification = useRequireEmailVerification() const navToWizard = useCallback(() => { control.close() navigation.navigate('StarterPackWizard', { fromDialog: true, targetDid: targetDid, onSuccess: () => { setTimeout(() => { if (!control.isOpen) { control.open() } }, 0) }, }) }, [navigation, control, targetDid]) const wrappedNavToWizard = requireEmailVerification(navToWizard, { instructions: [ Before creating a starter pack, you must first verify your email. , ], }) return ( ) } function Empty({onStartWizard}: {onStartWizard: () => void}) { const {_} = useLingui() const t = useTheme() return ( You have no starter packs. ) } function StarterPackList({ onStartWizard, targetDid, enabled, }: { onStartWizard: () => void targetDid: string enabled?: boolean }) { const control = Dialog.useDialogContext() const {_} = useLingui() const { data, isError, isLoading, hasNextPage, isFetchingNextPage, fetchNextPage, } = useActorStarterPacksWithMembershipsQuery({did: targetDid, enabled}) const membershipItems = data?.pages.flatMap(page => page.starterPacksWithMembership) || [] const onEndReached = useCallback(async () => { if (isFetchingNextPage || !hasNextPage || isError) return try { await fetchNextPage() } catch (err) { // Error handling is optional since this is just pagination } }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) const renderItem = useCallback( ({item}: {item: StarterPackWithMembership}) => ( ), [targetDid], ) const onClose = useCallback(() => { control.close() }, [control]) const listHeader = ( <> Add to starter packs {membershipItems.length > 0 && ( <> New starter pack )} ) return ( ( ) : renderItem } keyExtractor={ isLoading ? () => 'starter_pack_dialog_loader' : (item: StarterPackWithMembership) => item.starterPack.uri } onEndReached={onEndReached} onEndReachedThreshold={0.1} ListHeaderComponent={listHeader} ListEmptyComponent={} style={isWeb ? [a.px_md, {minHeight: 500}] : [a.px_2xl, a.pt_lg]} /> ) } function StarterPackItem({ starterPackWithMembership, targetDid, }: { starterPackWithMembership: StarterPackWithMembership targetDid: string }) { const {_} = useLingui() const t = useTheme() const queryClient = useQueryClient() const starterPack = starterPackWithMembership.starterPack const isInPack = !!starterPackWithMembership.listItem const [isPendingRefresh, setIsPendingRefresh] = useState(false) const {mutate: addMembership} = useListMembershipAddMutation({ onSuccess: () => { Toast.show(_(msg`Added to starter pack`)) // Use a timeout to wait for the appview to update, matching the pattern // in list-memberships.ts setTimeout(() => { invalidateActorStarterPacksWithMembershipQuery({ queryClient, did: targetDid, }) setIsPendingRefresh(false) }, 1e3) }, onError: () => { Toast.show(_(msg`Failed to add to starter pack`), 'xmark') setIsPendingRefresh(false) }, }) const {mutate: removeMembership} = useListMembershipRemoveMutation({ onSuccess: () => { Toast.show(_(msg`Removed from starter pack`)) // Use a timeout to wait for the appview to update, matching the pattern // in list-memberships.ts setTimeout(() => { invalidateActorStarterPacksWithMembershipQuery({ queryClient, did: targetDid, }) setIsPendingRefresh(false) }, 1e3) }, onError: () => { Toast.show(_(msg`Failed to remove from starter pack`), 'xmark') setIsPendingRefresh(false) }, }) const handleToggleMembership = () => { if (!starterPack.list?.uri || isPendingRefresh) return const listUri = starterPack.list.uri setIsPendingRefresh(true) if (!isInPack) { addMembership({ listUri: listUri, actorDid: targetDid, }) } else { if (!starterPackWithMembership.listItem?.uri) { console.error('Cannot remove: missing membership URI') setIsPendingRefresh(false) return } removeMembership({ listUri: listUri, actorDid: targetDid, membershipUri: starterPackWithMembership.listItem.uri, }) } } const {record} = starterPack if ( !bsky.dangerousIsType( record, AppBskyGraphStarterpack.isRecord, ) ) { return null } return ( {record.name} {starterPack.listItemsSample && starterPack.listItemsSample.length > 0 && ( <> p.subject)} /> {starterPack.list?.listItemCount && starterPack.list.listItemCount > 4 && ( )} )} ) }