From 7182cd3d5e157d7ad80f2e5c4a458730c46939a0 Mon Sep 17 00:00:00 2001 From: Chenyu Huang Date: Fri, 8 Aug 2025 15:33:45 -0700 Subject: starter pack dialog flow from profileMenu --- src/components/StarterPack/ProfileStarterPacks.tsx | 6 +- src/components/dialogs/StarterPackDialog.tsx | 388 +++++++++++++++++++++ 2 files changed, 391 insertions(+), 3 deletions(-) create mode 100644 src/components/dialogs/StarterPackDialog.tsx (limited to 'src/components') diff --git a/src/components/StarterPack/ProfileStarterPacks.tsx b/src/components/StarterPack/ProfileStarterPacks.tsx index de19b0bce..73aee28f4 100644 --- a/src/components/StarterPack/ProfileStarterPacks.tsx +++ b/src/components/StarterPack/ProfileStarterPacks.tsx @@ -180,7 +180,7 @@ function CreateAnother() { color="secondary" size="small" style={[a.self_center]} - onPress={() => navigation.navigate('StarterPackWizard')}> + onPress={() => navigation.navigate('StarterPackWizard', {})}> Create another @@ -238,7 +238,7 @@ function Empty() { ], }) const navToWizard = useCallback(() => { - navigation.navigate('StarterPackWizard') + navigation.navigate('StarterPackWizard', {}) }, [navigation]) const wrappedNavToWizard = requireEmailVerification(navToWizard, { instructions: [ @@ -322,7 +322,7 @@ function Empty() { color="secondary" cta={_(msg`Let me choose`)} onPress={() => { - navigation.navigate('StarterPackWizard') + navigation.navigate('StarterPackWizard', {}) }} /> diff --git a/src/components/dialogs/StarterPackDialog.tsx b/src/components/dialogs/StarterPackDialog.tsx new file mode 100644 index 000000000..efd157723 --- /dev/null +++ b/src/components/dialogs/StarterPackDialog.tsx @@ -0,0 +1,388 @@ +import React from 'react' +import {View} from 'react-native' +import { + type AppBskyGraphGetStarterPacksWithMembership, + AppBskyGraphStarterpack, +} from '@atproto/api' +import {msg, 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 { + RQKEY_WITH_MEMBERSHIP, + useActorStarterPacksWithMembershipsQuery, +} from '#/state/queries/actor-starter-packs' +import { + useListMembershipAddMutation, + useListMembershipRemoveMutation, +} from '#/state/queries/list-memberships' +import {List} from '#/view/com/util/List' +import * as Toast from '#/view/com/util/Toast' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {Divider} from '#/components/Divider' +import {Text} from '#/components/Typography' +import * as bsky from '#/types/bsky' +import {PlusLarge_Stroke2_Corner0_Rounded} from '../icons/Plus' +import {StarterPack} from '../icons/StarterPack' +import {TimesLarge_Stroke2_Corner0_Rounded} from '../icons/Times' + +type StarterPackWithMembership = + AppBskyGraphGetStarterPacksWithMembership.StarterPackWithMembership + +// Simple module-level state for dialog coordination +let dialogCallbacks: { + onSuccess?: () => void +} = {} + +export function notifyDialogSuccess() { + if (dialogCallbacks.onSuccess) { + dialogCallbacks.onSuccess() + } +} + +export type StarterPackDialogProps = { + control: Dialog.DialogControlProps + accountDid: string + targetDid: string + enabled?: boolean +} + +export function StarterPackDialog({ + control, + accountDid: _accountDid, + targetDid, + enabled, +}: StarterPackDialogProps) { + const {_} = useLingui() + const navigation = useNavigation() + const requireEmailVerification = useRequireEmailVerification() + + React.useEffect(() => { + dialogCallbacks.onSuccess = () => { + if (!control.isOpen) { + control.open() + } + } + }, [control]) + + const navToWizard = React.useCallback(() => { + control.close() + navigation.navigate('StarterPackWizard', {fromDialog: true}) + }, [navigation, control]) + + const wrappedNavToWizard = requireEmailVerification(navToWizard, { + instructions: [ + + Before creating a starter pack, you must first verify your email. + , + ], + }) + + const onClose = React.useCallback(() => { + // setCurrentView('initial') + control.close() + }, [control]) + + const t = useTheme() + + return ( + + + + + + + Add to starter packs + + + + + + + + + ) +} + +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 {_} = useLingui() + + const { + data, + refetch, + isError, + hasNextPage, + isFetchingNextPage, + fetchNextPage, + } = useActorStarterPacksWithMembershipsQuery({did: targetDid, enabled}) + + const membershipItems = + data?.pages.flatMap(page => page.starterPacksWithMembership) || [] + + const _onRefresh = React.useCallback(async () => { + try { + await refetch() + } catch (err) { + // Error handling is optional since this is just a refresh + } + }, [refetch]) + + const _onEndReached = React.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 = React.useCallback( + ({item}: {item: StarterPackWithMembership}) => ( + + ), + [targetDid], + ) + + const ListHeaderComponent = React.useCallback( + () => ( + <> + + + New starter pack + + + + + + ), + [_, onStartWizard], + ) + + return ( + + item.starterPack.uri || index.toString() + } + refreshing={false} + onRefresh={_onRefresh} + onEndReached={_onEndReached} + onEndReachedThreshold={0.1} + ListHeaderComponent={ + membershipItems.length > 0 ? ListHeaderComponent : null + } + ListEmptyComponent={} + /> + ) +} + +function StarterPackItem({ + starterPackWithMembership, + targetDid, +}: { + starterPackWithMembership: StarterPackWithMembership + targetDid: string +}) { + const {_} = useLingui() + const t = useTheme() + const queryClient = useQueryClient() + const [isUpdating, setIsUpdating] = React.useState(false) + + const starterPack = starterPackWithMembership.starterPack + const isInPack = !!starterPackWithMembership.listItem + console.log('StarterPackItem render. 111', { + starterPackWithMembership: starterPackWithMembership.listItem?.subject, + }) + + console.log('StarterPackItem render', { + starterPackWithMembership, + }) + + const {mutateAsync: addMembership} = useListMembershipAddMutation({ + onSuccess: () => { + Toast.show(_(msg`Added to starter pack`)) + }, + onError: () => { + Toast.show(_(msg`Failed to add to starter pack`), 'xmark') + }, + }) + + const {mutateAsync: removeMembership} = useListMembershipRemoveMutation({ + onSuccess: () => { + Toast.show(_(msg`Removed from starter pack`)) + }, + onError: () => { + Toast.show(_(msg`Failed to remove from starter pack`), 'xmark') + }, + }) + + const handleToggleMembership = async () => { + if (!starterPack.list?.uri || isUpdating) return + + const listUri = starterPack.list.uri + setIsUpdating(true) + + try { + if (!isInPack) { + await addMembership({ + listUri: listUri, + actorDid: targetDid, + }) + } else { + if (!starterPackWithMembership.listItem?.uri) { + console.error('Cannot remove: missing membership URI') + return + } + await removeMembership({ + listUri: listUri, + actorDid: targetDid, + membershipUri: starterPackWithMembership.listItem.uri, + }) + } + + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: RQKEY_WITH_MEMBERSHIP(targetDid), + }), + ]) + } catch (error) { + console.error('Failed to toggle membership:', error) + } finally { + setIsUpdating(false) + } + } + + const {record} = starterPack + + if ( + !bsky.dangerousIsType( + record, + AppBskyGraphStarterpack.isRecord, + ) + ) { + return null + } + + return ( + + + + {record.name} + + + + {starterPack.listItemsSample && + starterPack.listItemsSample.length > 0 && ( + <> + {starterPack.listItemsSample?.slice(0, 4).map((p, index) => ( + 0 ? -2 : 0, + borderWidth: 0.5, + borderColor: t.atoms.bg.backgroundColor, + }, + ]} + /> + ))} + + {starterPack.list?.listItemCount && + starterPack.list.listItemCount > 4 && ( + + {`+${starterPack.list.listItemCount - 4} more`} + + )} + + )} + + + + + + ) +} -- cgit 1.4.1