Merge pull request #8806 from internet-development/binaryfiddler/starter-pack-part2

Starter pack dialog implementations
This commit is contained in:
jim
2025-08-25 23:23:20 -07:00
committed by GitHub
10 changed files with 574 additions and 77 deletions
@@ -180,7 +180,7 @@ function CreateAnother() {
color="secondary"
size="small"
style={[a.self_center]}
onPress={() => navigation.navigate('StarterPackWizard')}>
onPress={() => navigation.navigate('StarterPackWizard', {})}>
<ButtonText>
<Trans>Create another</Trans>
</ButtonText>
@@ -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', {})
}}
/>
</Prompt.Actions>
@@ -11,7 +11,6 @@ import {useLingui} from '@lingui/react'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {isWeb} from '#/platform/detection'
import {useSession} from '#/state/session'
import {type ListMethods} from '#/view/com/util/List'
import {
type WizardAction,
@@ -48,7 +47,6 @@ export function WizardEditListDialog({
}) {
const {_} = useLingui()
const t = useTheme()
const {currentAccount} = useSession()
const initialNumToRender = useInitialNumToRender()
const listRef = useRef<ListMethods>(null)
@@ -56,10 +54,7 @@ export function WizardEditListDialog({
const getData = () => {
if (state.currentStep === 'Feeds') return state.feeds
return [
profile,
...state.profiles.filter(p => p.did !== currentAccount?.did),
]
return [profile, ...state.profiles.filter(p => p.did !== profile.did)]
}
const renderItem = ({item}: ListRenderItemInfo<any>) =>
@@ -131,10 +131,13 @@ export function WizardProfileCard({
}) {
const {currentAccount} = useSession()
const isMe = profile.did === currentAccount?.did
const included = isMe || state.profiles.some(p => p.did === profile.did)
// Determine the "main" profile for this starter pack - either targetDid or current account
const targetProfileDid = state.targetDid || currentAccount?.did
const isTarget = profile.did === targetProfileDid
const included = isTarget || state.profiles.some(p => p.did === profile.did)
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 displayName = profile.displayName
? sanitizeDisplayName(profile.displayName)
@@ -144,7 +147,7 @@ export function WizardProfileCard({
if (disabled) return
Keyboard.dismiss()
if (profile.did === currentAccount?.did) return
if (profile.did === targetProfileDid) return
if (!included) {
dispatch({type: 'AddProfile', profile})
@@ -0,0 +1,399 @@
import React 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 {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {Divider} from '#/components/Divider'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
import {AvatarStack} from '../AvatarStack'
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
export type StarterPackDialogProps = {
control: Dialog.DialogControlProps
targetDid: string
enabled?: boolean
}
export function StarterPackDialog({
control,
targetDid,
enabled,
}: StarterPackDialogProps) {
const {_} = useLingui()
const navigation = useNavigation<NavigationProp>()
const requireEmailVerification = useRequireEmailVerification()
const navToWizard = React.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: [
<Trans key="nav">
Before creating a starter pack, you must first verify your email.
</Trans>,
],
})
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<StarterPackList
control={control}
onStartWizard={wrappedNavToWizard}
targetDid={targetDid}
enabled={enabled}
/>
</Dialog.Outer>
)
}
function Empty({onStartWizard}: {onStartWizard: () => void}) {
const {_} = useLingui()
const t = useTheme()
isWeb
return (
<View style={[a.gap_2xl, {paddingTop: isWeb ? 100 : 64}]}>
<View style={[a.gap_xs, a.align_center]}>
<StarterPack
width={48}
fill={t.atoms.border_contrast_medium.borderColor}
/>
<Text style={[a.text_center]}>
<Trans>You have no starter packs.</Trans>
</Text>
</View>
<View style={[a.align_center]}>
<Button
label={_(msg`Create starter pack`)}
color="secondary_inverted"
size="small"
onPress={onStartWizard}>
<ButtonText>
<Trans comment="Text on button to create a new starter pack">
Create
</Trans>
</ButtonText>
<ButtonIcon icon={PlusLarge_Stroke2_Corner0_Rounded} />
</Button>
</View>
</View>
)
}
function StarterPackList({
control,
onStartWizard,
targetDid,
enabled,
}: {
control: Dialog.DialogControlProps
onStartWizard: () => void
targetDid: string
enabled?: boolean
}) {
const {_} = useLingui()
const t = useTheme()
const {
data,
refetch,
isError,
isLoading,
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}) => (
<StarterPackItem starterPackWithMembership={item} targetDid={targetDid} />
),
[targetDid],
)
const onClose = React.useCallback(() => {
control.close()
}, [control])
const XIcon = React.useMemo(() => {
return (
<TimesLarge_Stroke2_Corner0_Rounded
fill={t.atoms.text_contrast_medium.color}
/>
)
}, [t])
const listHeader = (
<>
<View
style={[
{justifyContent: 'space-between', flexDirection: 'row'},
isWeb ? a.mb_2xl : a.my_lg,
a.align_center,
]}>
<Text style={[a.text_lg, a.font_bold]}>
<Trans>Add to starter packs</Trans>
</Text>
<Button label={_(msg`Close`)} onPress={onClose}>
<ButtonIcon icon={() => XIcon} />
</Button>
</View>
{membershipItems.length > 0 && (
<>
<View
style={[a.flex_row, a.justify_between, a.align_center, a.py_md]}>
<Text style={[a.text_md, a.font_bold]}>
<Trans>New starter pack</Trans>
</Text>
<Button
label={_(msg`Create starter pack`)}
color="secondary_inverted"
size="small"
onPress={onStartWizard}>
<ButtonText>
<Trans comment="Text on button to create a new starter pack">
Create
</Trans>
</ButtonText>
<ButtonIcon icon={PlusLarge_Stroke2_Corner0_Rounded} />
</Button>
</View>
<Divider />
</>
)}
</>
)
return (
<Dialog.InnerFlatList
data={isLoading ? [{}] : membershipItems}
renderItem={
isLoading
? () => (
<View style={[a.align_center, a.py_2xl]}>
<Loader size="xl" />
</View>
)
: renderItem
}
keyExtractor={
isLoading
? () => 'starter_pack_dialog_loader'
: (item: StarterPackWithMembership) => item.starterPack.uri
}
refreshing={false}
onRefresh={_onRefresh}
onEndReached={_onEndReached}
onEndReachedThreshold={0.1}
ListHeaderComponent={listHeader}
ListEmptyComponent={<Empty onStartWizard={onStartWizard} />}
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] = React.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<AppBskyGraphStarterpack.Record>(
record,
AppBskyGraphStarterpack.isRecord,
)
) {
return null
}
return (
<View style={[a.flex_row, a.justify_between, a.align_center, a.py_md]}>
<View>
<Text emoji style={[a.text_md, a.font_bold]} numberOfLines={1}>
{record.name}
</Text>
<View style={[a.flex_row, a.align_center, a.mt_xs]}>
{starterPack.listItemsSample &&
starterPack.listItemsSample.length > 0 && (
<>
<AvatarStack
size={32}
profiles={starterPack.listItemsSample
?.slice(0, 4)
.map(p => p.subject)}
/>
{starterPack.list?.listItemCount &&
starterPack.list.listItemCount > 4 && (
<Text
style={[
a.text_sm,
t.atoms.text_contrast_medium,
a.ml_xs,
]}>
<Trans>
<Plural
value={starterPack.list.listItemCount - 4}
other="+# more"
/>
</Trans>
</Text>
)}
</>
)}
</View>
</View>
<Button
label={isInPack ? _(msg`Remove`) : _(msg`Add`)}
color={isInPack ? 'secondary' : 'primary'}
size="tiny"
disabled={isPendingRefresh}
onPress={handleToggleMembership}>
<ButtonText>
{isInPack ? <Trans>Remove</Trans> : <Trans>Add</Trans>}
</ButtonText>
</Button>
</View>
)
}
+8 -15
View File
@@ -1,10 +1,10 @@
import {
$Typed,
AppBskyActorDefs,
AppBskyGraphGetStarterPack,
BskyAgent,
ComAtprotoRepoApplyWrites,
Facet,
type $Typed,
type AppBskyActorDefs,
type AppBskyGraphGetStarterPack,
type BskyAgent,
type ComAtprotoRepoApplyWrites,
type Facet,
} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -15,7 +15,7 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {enforceLen} from '#/lib/strings/helpers'
import {useAgent} from '#/state/session'
import * as bsky from '#/types/bsky'
import type * as bsky from '#/types/bsky'
export const createStarterPackList = async ({
name,
@@ -46,14 +46,7 @@ export const createStarterPackList = async ({
if (!list) throw new Error('List creation failed')
await agent.com.atproto.repo.applyWrites({
repo: agent.session!.did,
writes: [
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})),
),
writes: profiles.map(p => createListItem({did: p.did, listUri: list.uri})),
})
return list
+5 -1
View File
@@ -79,7 +79,11 @@ export type CommonNavigatorParams = {
Start: {name: string; rkey: string}
StarterPack: {name: string; rkey: string; new?: boolean}
StarterPackShort: {code: string}
StarterPackWizard: undefined
StarterPackWizard: {
fromDialog?: boolean
targetDid?: string
onSuccess?: () => void
}
StarterPackEdit: {rkey?: string}
VideoFeed: VideoFeedSourceContext
}
+9 -8
View File
@@ -7,7 +7,6 @@ import {
import {msg, plural} from '@lingui/macro'
import {STARTER_PACK_MAX_SIZE} from '#/lib/constants'
import {useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import * as bsky from '#/types/bsky'
@@ -37,6 +36,7 @@ interface State {
processing: boolean
error?: string
transitionDirection: 'Backward' | 'Forward'
targetDid?: string
}
type TStateContext = [State, (action: Action) => void]
@@ -118,15 +118,17 @@ function reducer(state: State, action: Action): State {
export function Provider({
starterPack,
listItems,
targetProfile,
children,
}: {
starterPack?: AppBskyGraphDefs.StarterPackView
listItems?: AppBskyGraphDefs.ListItemView[]
targetProfile: bsky.profile.AnyProfileView
children: React.ReactNode
}) {
const {currentAccount} = useSession()
const createInitialState = (): State => {
const targetDid = targetProfile?.did
if (
starterPack &&
bsky.validate(starterPack.record, AppBskyGraphStarterpack.validateRecord)
@@ -136,23 +138,22 @@ export function Provider({
currentStep: 'Details',
name: starterPack.record.name,
description: starterPack.record.description,
profiles:
listItems
?.map(i => i.subject)
.filter(p => p.did !== currentAccount?.did) ?? [],
profiles: listItems?.map(i => i.subject) ?? [],
feeds: starterPack.feeds ?? [],
processing: false,
transitionDirection: 'Forward',
targetDid,
}
}
return {
canNext: true,
currentStep: 'Details',
profiles: [],
profiles: [targetProfile],
feeds: [],
processing: false,
transitionDirection: 'Forward',
targetDid,
}
}
+72 -36
View File
@@ -68,12 +68,19 @@ export function Wizard({
CommonNavigatorParams,
'StarterPackEdit' | 'StarterPackWizard'
>) {
const {rkey} = route.params ?? {}
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 onSuccess = 'onSuccess' in params ? params.onSuccess : 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,
@@ -91,7 +98,7 @@ export function Wizard({
data: profile,
isLoading: isLoadingProfile,
isError: isErrorProfile,
} = useProfileQuery({did: currentAccount?.did})
} = useProfileQuery({did: profileDid})
const isEdit = Boolean(rkey)
const isReady =
@@ -127,12 +134,17 @@ export function Wizard({
<Layout.Screen
testID="starterPackWizardScreen"
style={web([{minHeight: 0}, a.flex_1])}>
<Provider starterPack={starterPack} listItems={listItems}>
<Provider
starterPack={starterPack}
listItems={listItems}
targetProfile={profile}>
<WizardInner
currentStarterPack={starterPack}
currentListItems={listItems}
profile={profile}
moderationOpts={moderationOpts}
fromDialog={fromDialog}
onSuccess={onSuccess}
/>
</Provider>
</Layout.Screen>
@@ -144,17 +156,22 @@ function WizardInner({
currentListItems,
profile,
moderationOpts,
fromDialog,
onSuccess,
}: {
currentStarterPack?: AppBskyGraphDefs.StarterPackView
currentListItems?: AppBskyGraphDefs.ListItemView[]
profile: AppBskyActorDefs.ProfileViewDetailed
moderationOpts: ModerationOpts
fromDialog?: boolean
onSuccess?: () => void
}) {
const navigation = useNavigation<NavigationProp>()
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode()
const [state, dispatch] = useWizardState()
const {currentAccount} = useSession()
const {data: currentProfile} = useProfileQuery({
did: currentAccount?.did,
staleTime: 0,
@@ -213,11 +230,17 @@ function WizardInner({
})
Image.prefetch([getStarterPackOgCard(currentProfile!.did, rkey)])
dispatch({type: 'SetProcessing', processing: false})
navigation.replace('StarterPack', {
name: currentAccount!.handle,
rkey,
new: true,
})
if (fromDialog) {
navigation.goBack()
onSuccess?.()
} else {
navigation.replace('StarterPack', {
name: profile!.handle,
rkey,
new: true,
})
}
}
const onSuccessEdit = () => {
@@ -285,10 +308,7 @@ function WizardInner({
)
}
const items =
state.currentStep === 'Profiles'
? [profile, ...state.profiles]
: state.feeds
const items = state.currentStep === 'Profiles' ? state.profiles : state.feeds
const isEditEnabled =
(state.currentStep === 'Profiles' && items.length > 1) ||
@@ -340,11 +360,7 @@ function WizardInner({
</Container>
{state.currentStep !== 'Details' && (
<Footer
onNext={onNext}
nextBtnText={currUiStrings.nextBtn}
profile={profile}
/>
<Footer onNext={onNext} nextBtnText={currUiStrings.nextBtn} />
)}
<WizardEditListDialog
control={editDialogControl}
@@ -392,20 +408,15 @@ function Container({children}: {children: React.ReactNode}) {
function Footer({
onNext,
nextBtnText,
profile,
}: {
onNext: () => void
nextBtnText: string
profile: AppBskyActorDefs.ProfileViewDetailed
}) {
const t = useTheme()
const [state] = useWizardState()
const {bottom: bottomInset} = useSafeAreaInsets()
const items =
state.currentStep === 'Profiles'
? [profile, ...state.profiles]
: state.feeds
const {currentAccount} = useSession()
const items = state.currentStep === 'Profiles' ? state.profiles : state.feeds
const minimumItems = state.currentStep === 'Profiles' ? 8 : 0
@@ -471,19 +482,44 @@ function Footer({
<Text style={[a.text_center, textStyles]}>
{
items.length < 2 ? (
<Trans>
It's just you right now! Add more people to your starter pack
by searching above.
</Trans>
currentAccount?.did === items[0].did ? (
<Trans>
It's just you right now! Add more people to your starter
pack by searching above.
</Trans>
) : (
<Trans>
It's just{' '}
<Text style={[a.font_bold, textStyles]} emoji>
{getName(items[0])}{' '}
</Text>
right now! Add more people to your starter pack by searching
above.
</Trans>
)
) : items.length === 2 ? (
<Trans>
<Text style={[a.font_bold, textStyles]}>You</Text> and
<Text> </Text>
<Text style={[a.font_bold, textStyles]} emoji>
{getName(items[1] /* [0] is self, skip it */)}{' '}
</Text>
are included in your starter pack
</Trans>
currentAccount?.did === items[0].did ? (
<Trans>
<Text style={[a.font_bold, textStyles]}>You</Text> and
<Text> </Text>
<Text style={[a.font_bold, textStyles]} emoji>
{getName(items[1] /* [0] is self, skip it */)}{' '}
</Text>
are included in your starter pack
</Trans>
) : (
<Trans>
<Text style={[a.font_bold, textStyles]}>
{getName(items[0])}
</Text>{' '}
and
<Text> </Text>
<Text style={[a.font_bold, textStyles]} emoji>
{getName(items[1] /* [0] is self, skip it */)}{' '}
</Text>
are included in your starter pack
</Trans>
)
) : items.length > 2 ? (
<Trans context="profiles">
<Text style={[a.font_bold, textStyles]} emoji>
+53 -4
View File
@@ -1,15 +1,23 @@
import {AppBskyGraphGetActorStarterPacks} from '@atproto/api'
import {
InfiniteData,
QueryClient,
QueryKey,
type AppBskyGraphGetActorStarterPacks,
type AppBskyGraphGetStarterPacksWithMembership,
} from '@atproto/api'
import {
type InfiniteData,
type QueryClient,
type QueryKey,
useInfiniteQuery,
} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
export const RQKEY_ROOT = 'actor-starter-packs'
export const RQKEY_WITH_MEMBERSHIP_ROOT = 'actor-starter-packs-with-membership'
export const RQKEY = (did?: string) => [RQKEY_ROOT, did]
export const RQKEY_WITH_MEMBERSHIP = (did?: string) => [
RQKEY_WITH_MEMBERSHIP_ROOT,
did,
]
export function useActorStarterPacksQuery({
did,
@@ -42,6 +50,37 @@ export function useActorStarterPacksQuery({
})
}
export function useActorStarterPacksWithMembershipsQuery({
did,
enabled = true,
}: {
did?: string
enabled?: boolean
}) {
const agent = useAgent()
return useInfiniteQuery<
AppBskyGraphGetStarterPacksWithMembership.OutputSchema,
Error,
InfiniteData<AppBskyGraphGetStarterPacksWithMembership.OutputSchema>,
QueryKey,
string | undefined
>({
queryKey: RQKEY_WITH_MEMBERSHIP(did),
queryFn: async ({pageParam}: {pageParam?: string}) => {
const res = await agent.app.bsky.graph.getStarterPacksWithMembership({
actor: did!,
limit: 10,
cursor: pageParam,
})
return res.data
},
enabled: Boolean(did) && enabled,
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
})
}
export async function invalidateActorStarterPacksQuery({
queryClient,
did,
@@ -51,3 +90,13 @@ export async function invalidateActorStarterPacksQuery({
}) {
await queryClient.invalidateQueries({queryKey: RQKEY(did)})
}
export async function invalidateActorStarterPacksWithMembershipQuery({
queryClient,
did,
}: {
queryClient: QueryClient
did: string
}) {
await queryClient.invalidateQueries({queryKey: RQKEY_WITH_MEMBERSHIP(did)})
}
+17
View File
@@ -27,6 +27,7 @@ import {EventStopper} from '#/view/com/util/EventStopper'
import * as Toast from '#/view/com/util/Toast'
import {Button, ButtonIcon} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {StarterPackDialog} from '#/components/dialogs/StarterPackDialog'
import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox'
import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheckIcon} from '#/components/icons/CircleCheck'
@@ -45,6 +46,7 @@ import {
} from '#/components/icons/Person'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
import {StarterPack} from '#/components/icons/StarterPack'
import {EditLiveDialog} from '#/components/live/EditLiveDialog'
import {GoLiveDialog} from '#/components/live/GoLiveDialog'
import * as Menu from '#/components/Menu'
@@ -88,6 +90,7 @@ let ProfileMenu = ({
const blockPromptControl = Prompt.usePromptControl()
const loggedOutWarningPromptControl = Prompt.usePromptControl()
const goLiveDialogControl = useDialogControl()
const addToStarterPacksDialogControl = useDialogControl()
const showLoggedOutWarning = React.useMemo(() => {
return (
@@ -300,6 +303,15 @@ let ProfileMenu = ({
)}
</>
)}
<Menu.Item
testID="profileHeaderDropdownStarterPackAddRemoveBtn"
label={_(msg`Add to starter packs`)}
onPress={addToStarterPacksDialogControl.open}>
<Menu.ItemText>
<Trans>Add to starter packs</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={StarterPack} />
</Menu.Item>
<Menu.Item
testID="profileHeaderDropdownListAddRemoveBtn"
label={_(msg`Add to lists`)}
@@ -440,6 +452,11 @@ let ProfileMenu = ({
</Menu.Outer>
</Menu.Root>
<StarterPackDialog
control={addToStarterPacksDialogControl}
targetDid={profile.did}
/>
<ReportDialog
control={reportDialogControl}
subject={{