An ATproto social media client -- with an independent Appview.

parameterize the initial profile for starter pack profile select wizard screen

+115 -111
+1 -5
src/components/StarterPack/Wizard/WizardEditListDialog.tsx
··· 48 48 }) { 49 49 const {_} = useLingui() 50 50 const t = useTheme() 51 - const {currentAccount} = useSession() 52 51 const initialNumToRender = useInitialNumToRender() 53 52 54 53 const listRef = useRef<ListMethods>(null) ··· 56 55 const getData = () => { 57 56 if (state.currentStep === 'Feeds') return state.feeds 58 57 59 - return [ 60 - profile, 61 - ...state.profiles.filter(p => p.did !== currentAccount?.did), 62 - ] 58 + return [profile, ...state.profiles.filter(p => p.did !== profile.did)] 63 59 } 64 60 65 61 const renderItem = ({item}: ListRenderItemInfo<any>) =>
+7 -4
src/components/StarterPack/Wizard/WizardListCard.tsx
··· 131 131 }) { 132 132 const {currentAccount} = useSession() 133 133 134 - const isMe = profile.did === currentAccount?.did 135 - const included = isMe || state.profiles.some(p => p.did === profile.did) 134 + // Determine the "main" profile for this starter pack - either targetDid or current account 135 + const targetProfileDid = state.targetDid || currentAccount?.did 136 + const isTarget = profile.did === targetProfileDid 137 + const included = isTarget || state.profiles.some(p => p.did === profile.did) 136 138 const disabled = 137 - isMe || (!included && state.profiles.length >= STARTER_PACK_MAX_SIZE - 1) 139 + isTarget || 140 + (!included && state.profiles.length >= STARTER_PACK_MAX_SIZE - 1) 138 141 const moderationUi = moderateProfile(profile, moderationOpts).ui('avatar') 139 142 const displayName = profile.displayName 140 143 ? sanitizeDisplayName(profile.displayName) ··· 144 147 if (disabled) return 145 148 146 149 Keyboard.dismiss() 147 - if (profile.did === currentAccount?.did) return 150 + if (profile.did === targetProfileDid) return 148 151 149 152 if (!included) { 150 153 dispatch({type: 'AddProfile', profile})
+50 -60
src/components/dialogs/StarterPackDialog.tsx
··· 12 12 import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification' 13 13 import {type NavigationProp} from '#/lib/routes/types' 14 14 import { 15 - RQKEY_WITH_MEMBERSHIP, 15 + invalidateActorStarterPacksWithMembershipQuery, 16 16 useActorStarterPacksWithMembershipsQuery, 17 17 } from '#/state/queries/actor-starter-packs' 18 18 import { ··· 35 35 type StarterPackWithMembership = 36 36 AppBskyGraphGetStarterPacksWithMembership.StarterPackWithMembership 37 37 38 - // Simple module-level state for dialog coordination 39 38 let dialogCallbacks: { 40 39 onSuccess?: () => void 41 40 } = {} ··· 48 47 49 48 export type StarterPackDialogProps = { 50 49 control: Dialog.DialogControlProps 51 - accountDid: string 52 50 targetDid: string 53 51 enabled?: boolean 54 52 } 55 53 56 54 export function StarterPackDialog({ 57 55 control, 58 - accountDid: _accountDid, 59 56 targetDid, 60 57 enabled, 61 58 }: StarterPackDialogProps) { ··· 73 70 74 71 const navToWizard = React.useCallback(() => { 75 72 control.close() 76 - navigation.navigate('StarterPackWizard', {fromDialog: true}) 77 - }, [navigation, control]) 73 + navigation.navigate('StarterPackWizard', { 74 + fromDialog: true, 75 + targetDid: targetDid, 76 + }) 77 + }, [navigation, control, targetDid]) 78 78 79 79 const wrappedNavToWizard = requireEmailVerification(navToWizard, { 80 80 instructions: [ ··· 85 85 }) 86 86 87 87 const onClose = React.useCallback(() => { 88 - // setCurrentView('initial') 89 88 control.close() 90 89 }, [control]) 91 90 ··· 252 251 const {_} = useLingui() 253 252 const t = useTheme() 254 253 const queryClient = useQueryClient() 255 - const [isUpdating, setIsUpdating] = React.useState(false) 256 254 257 255 const starterPack = starterPackWithMembership.starterPack 258 256 const isInPack = !!starterPackWithMembership.listItem 259 - console.log('StarterPackItem render. 111', { 260 - starterPackWithMembership: starterPackWithMembership.listItem?.subject, 261 - }) 262 257 263 - console.log('StarterPackItem render', { 264 - starterPackWithMembership, 265 - }) 258 + const {mutate: addMembership, isPending: isAddingPending} = 259 + useListMembershipAddMutation({ 260 + onSuccess: () => { 261 + Toast.show(_(msg`Added to starter pack`)) 262 + invalidateActorStarterPacksWithMembershipQuery({ 263 + queryClient, 264 + did: targetDid, 265 + }) 266 + }, 267 + onError: () => { 268 + Toast.show(_(msg`Failed to add to starter pack`), 'xmark') 269 + }, 270 + }) 266 271 267 - const {mutateAsync: addMembership} = useListMembershipAddMutation({ 268 - onSuccess: () => { 269 - Toast.show(_(msg`Added to starter pack`)) 270 - }, 271 - onError: () => { 272 - Toast.show(_(msg`Failed to add to starter pack`), 'xmark') 273 - }, 274 - }) 272 + const {mutate: removeMembership, isPending: isRemovingPending} = 273 + useListMembershipRemoveMutation({ 274 + onSuccess: () => { 275 + Toast.show(_(msg`Removed from starter pack`)) 276 + invalidateActorStarterPacksWithMembershipQuery({ 277 + queryClient, 278 + did: targetDid, 279 + }) 280 + }, 281 + onError: () => { 282 + Toast.show(_(msg`Failed to remove from starter pack`), 'xmark') 283 + }, 284 + }) 275 285 276 - const {mutateAsync: removeMembership} = useListMembershipRemoveMutation({ 277 - onSuccess: () => { 278 - Toast.show(_(msg`Removed from starter pack`)) 279 - }, 280 - onError: () => { 281 - Toast.show(_(msg`Failed to remove from starter pack`), 'xmark') 282 - }, 283 - }) 286 + const isMutating = isAddingPending || isRemovingPending 284 287 285 - const handleToggleMembership = async () => { 286 - if (!starterPack.list?.uri || isUpdating) return 288 + const handleToggleMembership = () => { 289 + if (!starterPack.list?.uri || isMutating) return 287 290 288 291 const listUri = starterPack.list.uri 289 - setIsUpdating(true) 290 292 291 - try { 292 - if (!isInPack) { 293 - await addMembership({ 294 - listUri: listUri, 295 - actorDid: targetDid, 296 - }) 297 - } else { 298 - if (!starterPackWithMembership.listItem?.uri) { 299 - console.error('Cannot remove: missing membership URI') 300 - return 301 - } 302 - await removeMembership({ 303 - listUri: listUri, 304 - actorDid: targetDid, 305 - membershipUri: starterPackWithMembership.listItem.uri, 306 - }) 293 + if (!isInPack) { 294 + addMembership({ 295 + listUri: listUri, 296 + actorDid: targetDid, 297 + }) 298 + } else { 299 + if (!starterPackWithMembership.listItem?.uri) { 300 + console.error('Cannot remove: missing membership URI') 301 + return 307 302 } 308 - 309 - await Promise.all([ 310 - queryClient.invalidateQueries({ 311 - queryKey: RQKEY_WITH_MEMBERSHIP(targetDid), 312 - }), 313 - ]) 314 - } catch (error) { 315 - console.error('Failed to toggle membership:', error) 316 - } finally { 317 - setIsUpdating(false) 303 + removeMembership({ 304 + listUri: listUri, 305 + actorDid: targetDid, 306 + membershipUri: starterPackWithMembership.listItem.uri, 307 + }) 318 308 } 319 309 } 320 310 ··· 377 367 label={isInPack ? _(msg`Remove`) : _(msg`Add`)} 378 368 color={isInPack ? 'secondary' : 'primary'} 379 369 size="tiny" 380 - disabled={isUpdating} 370 + disabled={isMutating} 381 371 onPress={handleToggleMembership}> 382 372 <ButtonText> 383 373 {isInPack ? <Trans>Remove</Trans> : <Trans>Add</Trans>}
+8 -15
src/lib/generate-starterpack.ts
··· 1 1 import { 2 - $Typed, 3 - AppBskyActorDefs, 4 - AppBskyGraphGetStarterPack, 5 - BskyAgent, 6 - ComAtprotoRepoApplyWrites, 7 - Facet, 2 + type $Typed, 3 + type AppBskyActorDefs, 4 + type AppBskyGraphGetStarterPack, 5 + type BskyAgent, 6 + type ComAtprotoRepoApplyWrites, 7 + type Facet, 8 8 } from '@atproto/api' 9 9 import {msg} from '@lingui/macro' 10 10 import {useLingui} from '@lingui/react' ··· 15 15 import {sanitizeHandle} from '#/lib/strings/handles' 16 16 import {enforceLen} from '#/lib/strings/helpers' 17 17 import {useAgent} from '#/state/session' 18 - import * as bsky from '#/types/bsky' 18 + import type * as bsky from '#/types/bsky' 19 19 20 20 export const createStarterPackList = async ({ 21 21 name, ··· 46 46 if (!list) throw new Error('List creation failed') 47 47 await agent.com.atproto.repo.applyWrites({ 48 48 repo: agent.session!.did, 49 - writes: [ 50 - createListItem({did: agent.session!.did, listUri: list.uri}), 51 - ].concat( 52 - profiles 53 - // Ensure we don't have ourselves in this list twice 54 - .filter(p => p.did !== agent.session!.did) 55 - .map(p => createListItem({did: p.did, listUri: list.uri})), 56 - ), 49 + writes: profiles.map(p => createListItem({did: p.did, listUri: list.uri})), 57 50 }) 58 51 59 52 return list
+1 -1
src/lib/routes/types.ts
··· 79 79 Start: {name: string; rkey: string} 80 80 StarterPack: {name: string; rkey: string; new?: boolean} 81 81 StarterPackShort: {code: string} 82 - StarterPackWizard: {fromDialog?: boolean} 82 + StarterPackWizard: {fromDialog?: boolean; targetDid?: string} 83 83 StarterPackEdit: {rkey?: string} 84 84 VideoFeed: VideoFeedSourceContext 85 85 }
+9 -8
src/screens/StarterPack/Wizard/State.tsx
··· 7 7 import {msg, plural} from '@lingui/macro' 8 8 9 9 import {STARTER_PACK_MAX_SIZE} from '#/lib/constants' 10 - import {useSession} from '#/state/session' 11 10 import * as Toast from '#/view/com/util/Toast' 12 11 import * as bsky from '#/types/bsky' 13 12 ··· 37 36 processing: boolean 38 37 error?: string 39 38 transitionDirection: 'Backward' | 'Forward' 39 + targetDid?: string 40 40 } 41 41 42 42 type TStateContext = [State, (action: Action) => void] ··· 118 118 export function Provider({ 119 119 starterPack, 120 120 listItems, 121 + targetProfile, 121 122 children, 122 123 }: { 123 124 starterPack?: AppBskyGraphDefs.StarterPackView 124 125 listItems?: AppBskyGraphDefs.ListItemView[] 126 + targetProfile: bsky.profile.AnyProfileView 125 127 children: React.ReactNode 126 128 }) { 127 - const {currentAccount} = useSession() 128 - 129 129 const createInitialState = (): State => { 130 + const targetDid = targetProfile?.did 131 + 130 132 if ( 131 133 starterPack && 132 134 bsky.validate(starterPack.record, AppBskyGraphStarterpack.validateRecord) ··· 136 138 currentStep: 'Details', 137 139 name: starterPack.record.name, 138 140 description: starterPack.record.description, 139 - profiles: 140 - listItems 141 - ?.map(i => i.subject) 142 - .filter(p => p.did !== currentAccount?.did) ?? [], 141 + profiles: listItems?.map(i => i.subject) ?? [], 143 142 feeds: starterPack.feeds ?? [], 144 143 processing: false, 145 144 transitionDirection: 'Forward', 145 + targetDid, 146 146 } 147 147 } 148 148 149 149 return { 150 150 canNext: true, 151 151 currentStep: 'Details', 152 - profiles: [], 152 + profiles: [targetProfile], 153 153 feeds: [], 154 154 processing: false, 155 155 transitionDirection: 'Forward', 156 + targetDid, 156 157 } 157 158 } 158 159
+29 -18
src/screens/StarterPack/Wizard/index.tsx
··· 72 72 const params = route.params ?? {} 73 73 const rkey = 'rkey' in params ? params.rkey : undefined 74 74 const fromDialog = 'fromDialog' in params ? params.fromDialog : false 75 + const targetDid = 'targetDid' in params ? params.targetDid : undefined 75 76 const {currentAccount} = useSession() 76 77 const moderationOpts = useModerationOpts() 77 78 78 79 const {_} = useLingui() 80 + 81 + // Use targetDid if provided (from dialog), otherwise use current account 82 + const profileDid = targetDid || currentAccount!.did 79 83 80 84 const { 81 85 data: starterPack, ··· 94 98 data: profile, 95 99 isLoading: isLoadingProfile, 96 100 isError: isErrorProfile, 97 - } = useProfileQuery({did: currentAccount?.did}) 101 + } = useProfileQuery({did: profileDid}) 98 102 99 103 const isEdit = Boolean(rkey) 100 104 const isReady = ··· 130 134 <Layout.Screen 131 135 testID="starterPackWizardScreen" 132 136 style={web([{minHeight: 0}, a.flex_1])}> 133 - <Provider starterPack={starterPack} listItems={listItems}> 137 + <Provider 138 + starterPack={starterPack} 139 + listItems={listItems} 140 + targetProfile={profile}> 134 141 <WizardInner 135 142 currentStarterPack={starterPack} 136 143 currentListItems={listItems} ··· 228 235 } else { 229 236 // Original behavior for other entry points 230 237 navigation.replace('StarterPack', { 231 - name: currentAccount!.handle, 238 + name: profile!.handle, 232 239 rkey, 233 240 new: true, 234 241 }) ··· 245 252 navigation.goBack() 246 253 } else { 247 254 navigation.replace('StarterPack', { 248 - name: currentAccount!.handle, 255 + name: profile!.handle, 249 256 rkey: parsed!.rkey, 250 257 }) 251 258 } ··· 281 288 currentListItems: currentListItems, 282 289 }) 283 290 } else { 291 + console.log('Creating new starter pack: ', state.profiles) 284 292 createStarterPack({ 285 293 name: state.name?.trim() || getDefaultName(), 286 294 description: state.description?.trim(), ··· 306 314 ) 307 315 } 308 316 309 - const items = 310 - state.currentStep === 'Profiles' 311 - ? [profile, ...state.profiles] 312 - : state.feeds 317 + const items = state.currentStep === 'Profiles' ? state.profiles : state.feeds 313 318 314 319 const isEditEnabled = 315 320 (state.currentStep === 'Profiles' && items.length > 1) || ··· 413 418 function Footer({ 414 419 onNext, 415 420 nextBtnText, 416 - profile, 417 421 }: { 418 422 onNext: () => void 419 423 nextBtnText: string 420 - profile: AppBskyActorDefs.ProfileViewDetailed 421 424 }) { 422 425 const t = useTheme() 423 426 const [state] = useWizardState() 424 427 const {bottom: bottomInset} = useSafeAreaInsets() 425 - 426 - const items = 427 - state.currentStep === 'Profiles' 428 - ? [profile, ...state.profiles] 429 - : state.feeds 428 + const {currentAccount} = useSession() 429 + const items = state.currentStep === 'Profiles' ? state.profiles : state.feeds 430 430 431 431 const minimumItems = state.currentStep === 'Profiles' ? 8 : 0 432 432 ··· 493 493 { 494 494 items.length < 2 ? ( 495 495 <Trans> 496 - It's just you right now! Add more people to your starter pack 497 - by searching above. 496 + It's just{' '} 497 + <Text style={[a.font_bold, textStyles]} emoji> 498 + {currentAccount?.did === items[0].did 499 + ? 'you' 500 + : getName(items[0])}{' '} 501 + </Text> 502 + right now! Add more people to your starter pack by searching 503 + above. 498 504 </Trans> 499 505 ) : items.length === 2 ? ( 500 506 <Trans> 501 - <Text style={[a.font_bold, textStyles]}>You</Text> and 507 + <Text style={[a.font_bold, textStyles]}> 508 + {currentAccount?.did === items[0].did 509 + ? 'you' 510 + : getName(items[0])} 511 + </Text>{' '} 512 + and 502 513 <Text> </Text> 503 514 <Text style={[a.font_bold, textStyles]} emoji> 504 515 {getName(items[1] /* [0] is self, skip it */)}{' '}
+10
src/state/queries/actor-starter-packs.ts
··· 90 90 }) { 91 91 await queryClient.invalidateQueries({queryKey: RQKEY(did)}) 92 92 } 93 + 94 + export async function invalidateActorStarterPacksWithMembershipQuery({ 95 + queryClient, 96 + did, 97 + }: { 98 + queryClient: QueryClient 99 + did: string 100 + }) { 101 + await queryClient.invalidateQueries({queryKey: RQKEY_WITH_MEMBERSHIP(did)}) 102 + }