parameterize the initial profile for starter pack profile select wizard screen

This commit is contained in:
Chenyu Huang
2025-08-08 16:10:35 -07:00
parent 7182cd3d5e
commit f42b5831bb
8 changed files with 115 additions and 111 deletions
@@ -48,7 +48,6 @@ export function WizardEditListDialog({
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
const {currentAccount} = useSession()
const initialNumToRender = useInitialNumToRender() const initialNumToRender = useInitialNumToRender()
const listRef = useRef<ListMethods>(null) const listRef = useRef<ListMethods>(null)
@@ -56,10 +55,7 @@ export function WizardEditListDialog({
const getData = () => { const getData = () => {
if (state.currentStep === 'Feeds') return state.feeds if (state.currentStep === 'Feeds') return state.feeds
return [ return [profile, ...state.profiles.filter(p => p.did !== profile.did)]
profile,
...state.profiles.filter(p => p.did !== currentAccount?.did),
]
} }
const renderItem = ({item}: ListRenderItemInfo<any>) => const renderItem = ({item}: ListRenderItemInfo<any>) =>
@@ -131,10 +131,13 @@ export function WizardProfileCard({
}) { }) {
const {currentAccount} = useSession() const {currentAccount} = useSession()
const isMe = profile.did === currentAccount?.did // Determine the "main" profile for this starter pack - either targetDid or current account
const included = isMe || state.profiles.some(p => p.did === profile.did) const targetProfileDid = state.targetDid || currentAccount?.did
const isTarget = profile.did === targetProfileDid
const included = isTarget || state.profiles.some(p => p.did === profile.did)
const disabled = const disabled =
isMe || (!included && state.profiles.length >= STARTER_PACK_MAX_SIZE - 1) isTarget ||
(!included && state.profiles.length >= STARTER_PACK_MAX_SIZE - 1)
const moderationUi = moderateProfile(profile, moderationOpts).ui('avatar') const moderationUi = moderateProfile(profile, moderationOpts).ui('avatar')
const displayName = profile.displayName const displayName = profile.displayName
? sanitizeDisplayName(profile.displayName) ? sanitizeDisplayName(profile.displayName)
@@ -144,7 +147,7 @@ export function WizardProfileCard({
if (disabled) return if (disabled) return
Keyboard.dismiss() Keyboard.dismiss()
if (profile.did === currentAccount?.did) return if (profile.did === targetProfileDid) return
if (!included) { if (!included) {
dispatch({type: 'AddProfile', profile}) dispatch({type: 'AddProfile', profile})
+50 -60
View File
@@ -12,7 +12,7 @@ import {useQueryClient} from '@tanstack/react-query'
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification' import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {type NavigationProp} from '#/lib/routes/types' import {type NavigationProp} from '#/lib/routes/types'
import { import {
RQKEY_WITH_MEMBERSHIP, invalidateActorStarterPacksWithMembershipQuery,
useActorStarterPacksWithMembershipsQuery, useActorStarterPacksWithMembershipsQuery,
} from '#/state/queries/actor-starter-packs' } from '#/state/queries/actor-starter-packs'
import { import {
@@ -35,7 +35,6 @@ import {TimesLarge_Stroke2_Corner0_Rounded} from '../icons/Times'
type StarterPackWithMembership = type StarterPackWithMembership =
AppBskyGraphGetStarterPacksWithMembership.StarterPackWithMembership AppBskyGraphGetStarterPacksWithMembership.StarterPackWithMembership
// Simple module-level state for dialog coordination
let dialogCallbacks: { let dialogCallbacks: {
onSuccess?: () => void onSuccess?: () => void
} = {} } = {}
@@ -48,14 +47,12 @@ export function notifyDialogSuccess() {
export type StarterPackDialogProps = { export type StarterPackDialogProps = {
control: Dialog.DialogControlProps control: Dialog.DialogControlProps
accountDid: string
targetDid: string targetDid: string
enabled?: boolean enabled?: boolean
} }
export function StarterPackDialog({ export function StarterPackDialog({
control, control,
accountDid: _accountDid,
targetDid, targetDid,
enabled, enabled,
}: StarterPackDialogProps) { }: StarterPackDialogProps) {
@@ -73,8 +70,11 @@ export function StarterPackDialog({
const navToWizard = React.useCallback(() => { const navToWizard = React.useCallback(() => {
control.close() control.close()
navigation.navigate('StarterPackWizard', {fromDialog: true}) navigation.navigate('StarterPackWizard', {
}, [navigation, control]) fromDialog: true,
targetDid: targetDid,
})
}, [navigation, control, targetDid])
const wrappedNavToWizard = requireEmailVerification(navToWizard, { const wrappedNavToWizard = requireEmailVerification(navToWizard, {
instructions: [ instructions: [
@@ -85,7 +85,6 @@ export function StarterPackDialog({
}) })
const onClose = React.useCallback(() => { const onClose = React.useCallback(() => {
// setCurrentView('initial')
control.close() control.close()
}, [control]) }, [control])
@@ -252,69 +251,60 @@ function StarterPackItem({
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [isUpdating, setIsUpdating] = React.useState(false)
const starterPack = starterPackWithMembership.starterPack const starterPack = starterPackWithMembership.starterPack
const isInPack = !!starterPackWithMembership.listItem const isInPack = !!starterPackWithMembership.listItem
console.log('StarterPackItem render. 111', {
starterPackWithMembership: starterPackWithMembership.listItem?.subject,
})
console.log('StarterPackItem render', { const {mutate: addMembership, isPending: isAddingPending} =
starterPackWithMembership, useListMembershipAddMutation({
}) onSuccess: () => {
Toast.show(_(msg`Added to starter pack`))
invalidateActorStarterPacksWithMembershipQuery({
queryClient,
did: targetDid,
})
},
onError: () => {
Toast.show(_(msg`Failed to add to starter pack`), 'xmark')
},
})
const {mutateAsync: addMembership} = useListMembershipAddMutation({ const {mutate: removeMembership, isPending: isRemovingPending} =
onSuccess: () => { useListMembershipRemoveMutation({
Toast.show(_(msg`Added to starter pack`)) onSuccess: () => {
}, Toast.show(_(msg`Removed from starter pack`))
onError: () => { invalidateActorStarterPacksWithMembershipQuery({
Toast.show(_(msg`Failed to add to starter pack`), 'xmark') queryClient,
}, did: targetDid,
}) })
},
onError: () => {
Toast.show(_(msg`Failed to remove from starter pack`), 'xmark')
},
})
const {mutateAsync: removeMembership} = useListMembershipRemoveMutation({ const isMutating = isAddingPending || isRemovingPending
onSuccess: () => {
Toast.show(_(msg`Removed from starter pack`))
},
onError: () => {
Toast.show(_(msg`Failed to remove from starter pack`), 'xmark')
},
})
const handleToggleMembership = async () => { const handleToggleMembership = () => {
if (!starterPack.list?.uri || isUpdating) return if (!starterPack.list?.uri || isMutating) return
const listUri = starterPack.list.uri const listUri = starterPack.list.uri
setIsUpdating(true)
try { if (!isInPack) {
if (!isInPack) { addMembership({
await addMembership({ listUri: listUri,
listUri: listUri, actorDid: targetDid,
actorDid: targetDid, })
}) } else {
} else { if (!starterPackWithMembership.listItem?.uri) {
if (!starterPackWithMembership.listItem?.uri) { console.error('Cannot remove: missing membership URI')
console.error('Cannot remove: missing membership URI') return
return
}
await removeMembership({
listUri: listUri,
actorDid: targetDid,
membershipUri: starterPackWithMembership.listItem.uri,
})
} }
removeMembership({
await Promise.all([ listUri: listUri,
queryClient.invalidateQueries({ actorDid: targetDid,
queryKey: RQKEY_WITH_MEMBERSHIP(targetDid), membershipUri: starterPackWithMembership.listItem.uri,
}), })
])
} catch (error) {
console.error('Failed to toggle membership:', error)
} finally {
setIsUpdating(false)
} }
} }
@@ -377,7 +367,7 @@ function StarterPackItem({
label={isInPack ? _(msg`Remove`) : _(msg`Add`)} label={isInPack ? _(msg`Remove`) : _(msg`Add`)}
color={isInPack ? 'secondary' : 'primary'} color={isInPack ? 'secondary' : 'primary'}
size="tiny" size="tiny"
disabled={isUpdating} disabled={isMutating}
onPress={handleToggleMembership}> onPress={handleToggleMembership}>
<ButtonText> <ButtonText>
{isInPack ? <Trans>Remove</Trans> : <Trans>Add</Trans>} {isInPack ? <Trans>Remove</Trans> : <Trans>Add</Trans>}
+8 -15
View File
@@ -1,10 +1,10 @@
import { import {
$Typed, type $Typed,
AppBskyActorDefs, type AppBskyActorDefs,
AppBskyGraphGetStarterPack, type AppBskyGraphGetStarterPack,
BskyAgent, type BskyAgent,
ComAtprotoRepoApplyWrites, type ComAtprotoRepoApplyWrites,
Facet, type Facet,
} from '@atproto/api' } from '@atproto/api'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -15,7 +15,7 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {enforceLen} from '#/lib/strings/helpers' import {enforceLen} from '#/lib/strings/helpers'
import {useAgent} from '#/state/session' import {useAgent} from '#/state/session'
import * as bsky from '#/types/bsky' import type * as bsky from '#/types/bsky'
export const createStarterPackList = async ({ export const createStarterPackList = async ({
name, name,
@@ -46,14 +46,7 @@ export const createStarterPackList = async ({
if (!list) throw new Error('List creation failed') if (!list) throw new Error('List creation failed')
await agent.com.atproto.repo.applyWrites({ await agent.com.atproto.repo.applyWrites({
repo: agent.session!.did, repo: agent.session!.did,
writes: [ writes: profiles.map(p => createListItem({did: p.did, listUri: list.uri})),
createListItem({did: agent.session!.did, listUri: list.uri}),
].concat(
profiles
// Ensure we don't have ourselves in this list twice
.filter(p => p.did !== agent.session!.did)
.map(p => createListItem({did: p.did, listUri: list.uri})),
),
}) })
return list return list
+1 -1
View File
@@ -79,7 +79,7 @@ export type CommonNavigatorParams = {
Start: {name: string; rkey: string} Start: {name: string; rkey: string}
StarterPack: {name: string; rkey: string; new?: boolean} StarterPack: {name: string; rkey: string; new?: boolean}
StarterPackShort: {code: string} StarterPackShort: {code: string}
StarterPackWizard: {fromDialog?: boolean} StarterPackWizard: {fromDialog?: boolean; targetDid?: string}
StarterPackEdit: {rkey?: string} StarterPackEdit: {rkey?: string}
VideoFeed: VideoFeedSourceContext VideoFeed: VideoFeedSourceContext
} }
+9 -8
View File
@@ -7,7 +7,6 @@ import {
import {msg, plural} from '@lingui/macro' import {msg, plural} from '@lingui/macro'
import {STARTER_PACK_MAX_SIZE} from '#/lib/constants' import {STARTER_PACK_MAX_SIZE} from '#/lib/constants'
import {useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import * as bsky from '#/types/bsky' import * as bsky from '#/types/bsky'
@@ -37,6 +36,7 @@ interface State {
processing: boolean processing: boolean
error?: string error?: string
transitionDirection: 'Backward' | 'Forward' transitionDirection: 'Backward' | 'Forward'
targetDid?: string
} }
type TStateContext = [State, (action: Action) => void] type TStateContext = [State, (action: Action) => void]
@@ -118,15 +118,17 @@ function reducer(state: State, action: Action): State {
export function Provider({ export function Provider({
starterPack, starterPack,
listItems, listItems,
targetProfile,
children, children,
}: { }: {
starterPack?: AppBskyGraphDefs.StarterPackView starterPack?: AppBskyGraphDefs.StarterPackView
listItems?: AppBskyGraphDefs.ListItemView[] listItems?: AppBskyGraphDefs.ListItemView[]
targetProfile: bsky.profile.AnyProfileView
children: React.ReactNode children: React.ReactNode
}) { }) {
const {currentAccount} = useSession()
const createInitialState = (): State => { const createInitialState = (): State => {
const targetDid = targetProfile?.did
if ( if (
starterPack && starterPack &&
bsky.validate(starterPack.record, AppBskyGraphStarterpack.validateRecord) bsky.validate(starterPack.record, AppBskyGraphStarterpack.validateRecord)
@@ -136,23 +138,22 @@ export function Provider({
currentStep: 'Details', currentStep: 'Details',
name: starterPack.record.name, name: starterPack.record.name,
description: starterPack.record.description, description: starterPack.record.description,
profiles: profiles: listItems?.map(i => i.subject) ?? [],
listItems
?.map(i => i.subject)
.filter(p => p.did !== currentAccount?.did) ?? [],
feeds: starterPack.feeds ?? [], feeds: starterPack.feeds ?? [],
processing: false, processing: false,
transitionDirection: 'Forward', transitionDirection: 'Forward',
targetDid,
} }
} }
return { return {
canNext: true, canNext: true,
currentStep: 'Details', currentStep: 'Details',
profiles: [], profiles: [targetProfile],
feeds: [], feeds: [],
processing: false, processing: false,
transitionDirection: 'Forward', transitionDirection: 'Forward',
targetDid,
} }
} }
+29 -18
View File
@@ -72,11 +72,15 @@ export function Wizard({
const params = route.params ?? {} const params = route.params ?? {}
const rkey = 'rkey' in params ? params.rkey : undefined const rkey = 'rkey' in params ? params.rkey : undefined
const fromDialog = 'fromDialog' in params ? params.fromDialog : false const fromDialog = 'fromDialog' in params ? params.fromDialog : false
const targetDid = 'targetDid' in params ? params.targetDid : undefined
const {currentAccount} = useSession() const {currentAccount} = useSession()
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const {_} = useLingui() const {_} = useLingui()
// Use targetDid if provided (from dialog), otherwise use current account
const profileDid = targetDid || currentAccount!.did
const { const {
data: starterPack, data: starterPack,
isLoading: isLoadingStarterPack, isLoading: isLoadingStarterPack,
@@ -94,7 +98,7 @@ export function Wizard({
data: profile, data: profile,
isLoading: isLoadingProfile, isLoading: isLoadingProfile,
isError: isErrorProfile, isError: isErrorProfile,
} = useProfileQuery({did: currentAccount?.did}) } = useProfileQuery({did: profileDid})
const isEdit = Boolean(rkey) const isEdit = Boolean(rkey)
const isReady = const isReady =
@@ -130,7 +134,10 @@ export function Wizard({
<Layout.Screen <Layout.Screen
testID="starterPackWizardScreen" testID="starterPackWizardScreen"
style={web([{minHeight: 0}, a.flex_1])}> style={web([{minHeight: 0}, a.flex_1])}>
<Provider starterPack={starterPack} listItems={listItems}> <Provider
starterPack={starterPack}
listItems={listItems}
targetProfile={profile}>
<WizardInner <WizardInner
currentStarterPack={starterPack} currentStarterPack={starterPack}
currentListItems={listItems} currentListItems={listItems}
@@ -228,7 +235,7 @@ function WizardInner({
} else { } else {
// Original behavior for other entry points // Original behavior for other entry points
navigation.replace('StarterPack', { navigation.replace('StarterPack', {
name: currentAccount!.handle, name: profile!.handle,
rkey, rkey,
new: true, new: true,
}) })
@@ -245,7 +252,7 @@ function WizardInner({
navigation.goBack() navigation.goBack()
} else { } else {
navigation.replace('StarterPack', { navigation.replace('StarterPack', {
name: currentAccount!.handle, name: profile!.handle,
rkey: parsed!.rkey, rkey: parsed!.rkey,
}) })
} }
@@ -281,6 +288,7 @@ function WizardInner({
currentListItems: currentListItems, currentListItems: currentListItems,
}) })
} else { } else {
console.log('Creating new starter pack: ', state.profiles)
createStarterPack({ createStarterPack({
name: state.name?.trim() || getDefaultName(), name: state.name?.trim() || getDefaultName(),
description: state.description?.trim(), description: state.description?.trim(),
@@ -306,10 +314,7 @@ function WizardInner({
) )
} }
const items = const items = state.currentStep === 'Profiles' ? state.profiles : state.feeds
state.currentStep === 'Profiles'
? [profile, ...state.profiles]
: state.feeds
const isEditEnabled = const isEditEnabled =
(state.currentStep === 'Profiles' && items.length > 1) || (state.currentStep === 'Profiles' && items.length > 1) ||
@@ -413,20 +418,15 @@ function Container({children}: {children: React.ReactNode}) {
function Footer({ function Footer({
onNext, onNext,
nextBtnText, nextBtnText,
profile,
}: { }: {
onNext: () => void onNext: () => void
nextBtnText: string nextBtnText: string
profile: AppBskyActorDefs.ProfileViewDetailed
}) { }) {
const t = useTheme() const t = useTheme()
const [state] = useWizardState() const [state] = useWizardState()
const {bottom: bottomInset} = useSafeAreaInsets() const {bottom: bottomInset} = useSafeAreaInsets()
const {currentAccount} = useSession()
const items = const items = state.currentStep === 'Profiles' ? state.profiles : state.feeds
state.currentStep === 'Profiles'
? [profile, ...state.profiles]
: state.feeds
const minimumItems = state.currentStep === 'Profiles' ? 8 : 0 const minimumItems = state.currentStep === 'Profiles' ? 8 : 0
@@ -493,12 +493,23 @@ function Footer({
{ {
items.length < 2 ? ( items.length < 2 ? (
<Trans> <Trans>
It's just you right now! Add more people to your starter pack It's just{' '}
by searching above. <Text style={[a.font_bold, textStyles]} emoji>
{currentAccount?.did === items[0].did
? 'you'
: getName(items[0])}{' '}
</Text>
right now! Add more people to your starter pack by searching
above.
</Trans> </Trans>
) : items.length === 2 ? ( ) : items.length === 2 ? (
<Trans> <Trans>
<Text style={[a.font_bold, textStyles]}>You</Text> and <Text style={[a.font_bold, textStyles]}>
{currentAccount?.did === items[0].did
? 'you'
: getName(items[0])}
</Text>{' '}
and
<Text> </Text> <Text> </Text>
<Text style={[a.font_bold, textStyles]} emoji> <Text style={[a.font_bold, textStyles]} emoji>
{getName(items[1] /* [0] is self, skip it */)}{' '} {getName(items[1] /* [0] is self, skip it */)}{' '}
+10
View File
@@ -90,3 +90,13 @@ export async function invalidateActorStarterPacksQuery({
}) { }) {
await queryClient.invalidateQueries({queryKey: RQKEY(did)}) await queryClient.invalidateQueries({queryKey: RQKEY(did)})
} }
export async function invalidateActorStarterPacksWithMembershipQuery({
queryClient,
did,
}: {
queryClient: QueryClient
did: string
}) {
await queryClient.invalidateQueries({queryKey: RQKEY_WITH_MEMBERSHIP(did)})
}