import React from 'react' import {Keyboard, View} from 'react-native' import {KeyboardAwareScrollView} from 'react-native-keyboard-controller' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {Image} from 'expo-image' import { type AppBskyActorDefs, type AppBskyFeedDefs, type AppBskyGraphDefs, AtUri, type ModerationOpts, } from '@atproto/api' import {msg, Plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useFocusEffect, useNavigation} from '@react-navigation/native' import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {STARTER_PACK_MAX_SIZE} from '#/lib/constants' import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import { type CommonNavigatorParams, type NavigationProp, } from '#/lib/routes/types' import {logEvent} from '#/lib/statsig/statsig' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {enforceLen} from '#/lib/strings/helpers' import { getStarterPackOgCard, parseStarterPackUri, } from '#/lib/strings/starter-pack' import {logger} from '#/logger' import {isNative} from '#/platform/detection' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useAllListMembersQuery} from '#/state/queries/list-members' import {useProfileQuery} from '#/state/queries/profile' import { useCreateStarterPackMutation, useEditStarterPackMutation, useStarterPackQuery, } from '#/state/queries/starter-packs' import {useSession} from '#/state/session' import {useSetMinimalShellMode} from '#/state/shell' import * as Toast from '#/view/com/util/Toast' import {UserAvatar} from '#/view/com/util/UserAvatar' import { useWizardState, type WizardStep, } from '#/screens/StarterPack/Wizard/State' import {StepDetails} from '#/screens/StarterPack/Wizard/StepDetails' import {StepFeeds} from '#/screens/StarterPack/Wizard/StepFeeds' import {StepProfiles} from '#/screens/StarterPack/Wizard/StepProfiles' import {atoms as a, useTheme, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {useDialogControl} from '#/components/Dialog' import {notifyDialogSuccess} from '#/components/dialogs/StarterPackDialog' import * as Layout from '#/components/Layout' import {ListMaybePlaceholder} from '#/components/Lists' import {Loader} from '#/components/Loader' import {WizardEditListDialog} from '#/components/StarterPack/Wizard/WizardEditListDialog' import {Text} from '#/components/Typography' import type * as bsky from '#/types/bsky' import {Provider} from './State' export function Wizard({ route, }: NativeStackScreenProps< CommonNavigatorParams, 'StarterPackEdit' | 'StarterPackWizard' >) { const params = route.params ?? {} const rkey = 'rkey' in params ? params.rkey : undefined const fromDialog = 'fromDialog' in params ? params.fromDialog : false const targetDid = 'targetDid' in params ? params.targetDid : undefined const {currentAccount} = useSession() const moderationOpts = useModerationOpts() const {_} = useLingui() // Use targetDid if provided (from dialog), otherwise use current account const profileDid = targetDid || currentAccount!.did const { data: starterPack, isLoading: isLoadingStarterPack, isError: isErrorStarterPack, } = useStarterPackQuery({did: currentAccount!.did, rkey}) const listUri = starterPack?.list?.uri const { data: listItems, isLoading: isLoadingProfiles, isError: isErrorProfiles, } = useAllListMembersQuery(listUri) const { data: profile, isLoading: isLoadingProfile, isError: isErrorProfile, } = useProfileQuery({did: profileDid}) const isEdit = Boolean(rkey) const isReady = (!isEdit || (isEdit && starterPack && listItems)) && profile && moderationOpts if (!isReady) { return ( ) } else if (isEdit && starterPack?.creator.did !== currentAccount?.did) { return ( ) } return ( ) } function WizardInner({ currentStarterPack, currentListItems, profile, moderationOpts, fromDialog, }: { currentStarterPack?: AppBskyGraphDefs.StarterPackView currentListItems?: AppBskyGraphDefs.ListItemView[] profile: AppBskyActorDefs.ProfileViewDetailed moderationOpts: ModerationOpts fromDialog?: boolean }) { const navigation = useNavigation() const {_} = useLingui() const setMinimalShellMode = useSetMinimalShellMode() const [state, dispatch] = useWizardState() const {currentAccount} = useSession() const {data: currentProfile} = useProfileQuery({ did: currentAccount?.did, staleTime: 0, }) const parsed = parseStarterPackUri(currentStarterPack?.uri) React.useEffect(() => { navigation.setOptions({ gestureEnabled: false, }) }, [navigation]) useEnableKeyboardControllerScreen(true) useFocusEffect( React.useCallback(() => { setMinimalShellMode(true) return () => { setMinimalShellMode(false) } }, [setMinimalShellMode]), ) const getDefaultName = () => { const displayName = createSanitizedDisplayName(currentProfile!, true) return _(msg`${displayName}'s Starter Pack`).slice(0, 50) } const wizardUiStrings: Record< WizardStep, {header: string; nextBtn: string; subtitle?: string} > = { Details: { header: _(msg`Starter Pack`), nextBtn: _(msg`Next`), }, Profiles: { header: _(msg`Choose People`), nextBtn: _(msg`Next`), }, Feeds: { header: _(msg`Choose Feeds`), nextBtn: state.feeds.length === 0 ? _(msg`Skip`) : _(msg`Finish`), }, } const currUiStrings = wizardUiStrings[state.currentStep] const onSuccessCreate = (data: {uri: string; cid: string}) => { const rkey = new AtUri(data.uri).rkey logEvent('starterPack:create', { setName: state.name != null, setDescription: state.description != null, profilesCount: state.profiles.length, feedsCount: state.feeds.length, }) Image.prefetch([getStarterPackOgCard(currentProfile!.did, rkey)]) dispatch({type: 'SetProcessing', processing: false}) // If launched from ProfileMenu dialog, notify the dialog and go back if (fromDialog) { navigation.goBack() notifyDialogSuccess() } else { // Original behavior for other entry points navigation.replace('StarterPack', { name: profile!.handle, rkey, new: true, }) } } const onSuccessEdit = () => { // If launched from ProfileMenu dialog, go back to stay on profile page if (fromDialog) { navigation.goBack() } else { // Original behavior for other entry points if (navigation.canGoBack()) { navigation.goBack() } else { navigation.replace('StarterPack', { name: profile!.handle, rkey: parsed!.rkey, }) } } } const {mutate: createStarterPack} = useCreateStarterPackMutation({ onSuccess: onSuccessCreate, onError: e => { logger.error('Failed to create starter pack', {safeMessage: e}) dispatch({type: 'SetProcessing', processing: false}) Toast.show(_(msg`Failed to create starter pack`), 'xmark') }, }) const {mutate: editStarterPack} = useEditStarterPackMutation({ onSuccess: onSuccessEdit, onError: e => { logger.error('Failed to edit starter pack', {safeMessage: e}) dispatch({type: 'SetProcessing', processing: false}) Toast.show(_(msg`Failed to create starter pack`), 'xmark') }, }) const submit = async () => { dispatch({type: 'SetProcessing', processing: true}) if (currentStarterPack && currentListItems) { editStarterPack({ name: state.name?.trim() || getDefaultName(), description: state.description?.trim(), profiles: state.profiles, feeds: state.feeds, currentStarterPack: currentStarterPack, currentListItems: currentListItems, }) } else { console.log('Creating new starter pack: ', state.profiles) createStarterPack({ name: state.name?.trim() || getDefaultName(), description: state.description?.trim(), profiles: state.profiles, feeds: state.feeds, }) } } const onNext = () => { if (state.currentStep === 'Feeds') { submit() return } const keyboardVisible = Keyboard.isVisible() Keyboard.dismiss() setTimeout( () => { dispatch({type: 'Next'}) }, keyboardVisible ? 16 : 0, ) } const items = state.currentStep === 'Profiles' ? state.profiles : state.feeds const isEditEnabled = (state.currentStep === 'Profiles' && items.length > 1) || (state.currentStep === 'Feeds' && items.length > 0) const editDialogControl = useDialogControl() return ( { if (state.currentStep !== 'Details') { evt.preventDefault() dispatch({type: 'Back'}) } }} /> {currUiStrings.header} {isEditEnabled ? ( ) : ( )} {state.currentStep === 'Details' ? ( ) : state.currentStep === 'Profiles' ? ( ) : state.currentStep === 'Feeds' ? ( ) : null} {state.currentStep !== 'Details' && (