Merge pull request #8806 from internet-development/binaryfiddler/starter-pack-part2
Starter pack dialog implementations
This commit is contained in:
@@ -180,7 +180,7 @@ function CreateAnother() {
|
|||||||
color="secondary"
|
color="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
style={[a.self_center]}
|
style={[a.self_center]}
|
||||||
onPress={() => navigation.navigate('StarterPackWizard')}>
|
onPress={() => navigation.navigate('StarterPackWizard', {})}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Create another</Trans>
|
<Trans>Create another</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
@@ -238,7 +238,7 @@ function Empty() {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
const navToWizard = useCallback(() => {
|
const navToWizard = useCallback(() => {
|
||||||
navigation.navigate('StarterPackWizard')
|
navigation.navigate('StarterPackWizard', {})
|
||||||
}, [navigation])
|
}, [navigation])
|
||||||
const wrappedNavToWizard = requireEmailVerification(navToWizard, {
|
const wrappedNavToWizard = requireEmailVerification(navToWizard, {
|
||||||
instructions: [
|
instructions: [
|
||||||
@@ -322,7 +322,7 @@ function Empty() {
|
|||||||
color="secondary"
|
color="secondary"
|
||||||
cta={_(msg`Let me choose`)}
|
cta={_(msg`Let me choose`)}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
navigation.navigate('StarterPackWizard')
|
navigation.navigate('StarterPackWizard', {})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Prompt.Actions>
|
</Prompt.Actions>
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {useLingui} from '@lingui/react'
|
|||||||
|
|
||||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||||
import {isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import {useSession} from '#/state/session'
|
|
||||||
import {type ListMethods} from '#/view/com/util/List'
|
import {type ListMethods} from '#/view/com/util/List'
|
||||||
import {
|
import {
|
||||||
type WizardAction,
|
type WizardAction,
|
||||||
@@ -48,7 +47,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 +54,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})
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -79,7 +79,11 @@ 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: undefined
|
StarterPackWizard: {
|
||||||
|
fromDialog?: boolean
|
||||||
|
targetDid?: string
|
||||||
|
onSuccess?: () => void
|
||||||
|
}
|
||||||
StarterPackEdit: {rkey?: string}
|
StarterPackEdit: {rkey?: string}
|
||||||
VideoFeed: VideoFeedSourceContext
|
VideoFeed: VideoFeedSourceContext
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,12 +68,19 @@ export function Wizard({
|
|||||||
CommonNavigatorParams,
|
CommonNavigatorParams,
|
||||||
'StarterPackEdit' | 'StarterPackWizard'
|
'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 {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,
|
||||||
@@ -91,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 =
|
||||||
@@ -127,12 +134,17 @@ 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}
|
||||||
profile={profile}
|
profile={profile}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
|
fromDialog={fromDialog}
|
||||||
|
onSuccess={onSuccess}
|
||||||
/>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
</Layout.Screen>
|
</Layout.Screen>
|
||||||
@@ -144,17 +156,22 @@ function WizardInner({
|
|||||||
currentListItems,
|
currentListItems,
|
||||||
profile,
|
profile,
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
|
fromDialog,
|
||||||
|
onSuccess,
|
||||||
}: {
|
}: {
|
||||||
currentStarterPack?: AppBskyGraphDefs.StarterPackView
|
currentStarterPack?: AppBskyGraphDefs.StarterPackView
|
||||||
currentListItems?: AppBskyGraphDefs.ListItemView[]
|
currentListItems?: AppBskyGraphDefs.ListItemView[]
|
||||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
|
fromDialog?: boolean
|
||||||
|
onSuccess?: () => void
|
||||||
}) {
|
}) {
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
const [state, dispatch] = useWizardState()
|
const [state, dispatch] = useWizardState()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
const {data: currentProfile} = useProfileQuery({
|
const {data: currentProfile} = useProfileQuery({
|
||||||
did: currentAccount?.did,
|
did: currentAccount?.did,
|
||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
@@ -213,11 +230,17 @@ function WizardInner({
|
|||||||
})
|
})
|
||||||
Image.prefetch([getStarterPackOgCard(currentProfile!.did, rkey)])
|
Image.prefetch([getStarterPackOgCard(currentProfile!.did, rkey)])
|
||||||
dispatch({type: 'SetProcessing', processing: false})
|
dispatch({type: 'SetProcessing', processing: false})
|
||||||
navigation.replace('StarterPack', {
|
|
||||||
name: currentAccount!.handle,
|
if (fromDialog) {
|
||||||
rkey,
|
navigation.goBack()
|
||||||
new: true,
|
onSuccess?.()
|
||||||
})
|
} else {
|
||||||
|
navigation.replace('StarterPack', {
|
||||||
|
name: profile!.handle,
|
||||||
|
rkey,
|
||||||
|
new: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSuccessEdit = () => {
|
const onSuccessEdit = () => {
|
||||||
@@ -285,10 +308,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) ||
|
||||||
@@ -340,11 +360,7 @@ function WizardInner({
|
|||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
{state.currentStep !== 'Details' && (
|
{state.currentStep !== 'Details' && (
|
||||||
<Footer
|
<Footer onNext={onNext} nextBtnText={currUiStrings.nextBtn} />
|
||||||
onNext={onNext}
|
|
||||||
nextBtnText={currUiStrings.nextBtn}
|
|
||||||
profile={profile}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<WizardEditListDialog
|
<WizardEditListDialog
|
||||||
control={editDialogControl}
|
control={editDialogControl}
|
||||||
@@ -392,20 +408,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
|
||||||
|
|
||||||
@@ -471,19 +482,44 @@ function Footer({
|
|||||||
<Text style={[a.text_center, textStyles]}>
|
<Text style={[a.text_center, textStyles]}>
|
||||||
{
|
{
|
||||||
items.length < 2 ? (
|
items.length < 2 ? (
|
||||||
<Trans>
|
currentAccount?.did === items[0].did ? (
|
||||||
It's just you right now! Add more people to your starter pack
|
<Trans>
|
||||||
by searching above.
|
It's just you right now! Add more people to your starter
|
||||||
</Trans>
|
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 ? (
|
) : items.length === 2 ? (
|
||||||
<Trans>
|
currentAccount?.did === items[0].did ? (
|
||||||
<Text style={[a.font_bold, textStyles]}>You</Text> and
|
<Trans>
|
||||||
<Text> </Text>
|
<Text style={[a.font_bold, textStyles]}>You</Text> and
|
||||||
<Text style={[a.font_bold, textStyles]} emoji>
|
<Text> </Text>
|
||||||
{getName(items[1] /* [0] is self, skip it */)}{' '}
|
<Text style={[a.font_bold, textStyles]} emoji>
|
||||||
</Text>
|
{getName(items[1] /* [0] is self, skip it */)}{' '}
|
||||||
are included in your starter pack
|
</Text>
|
||||||
</Trans>
|
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 ? (
|
) : items.length > 2 ? (
|
||||||
<Trans context="profiles">
|
<Trans context="profiles">
|
||||||
<Text style={[a.font_bold, textStyles]} emoji>
|
<Text style={[a.font_bold, textStyles]} emoji>
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
import {AppBskyGraphGetActorStarterPacks} from '@atproto/api'
|
|
||||||
import {
|
import {
|
||||||
InfiniteData,
|
type AppBskyGraphGetActorStarterPacks,
|
||||||
QueryClient,
|
type AppBskyGraphGetStarterPacksWithMembership,
|
||||||
QueryKey,
|
} from '@atproto/api'
|
||||||
|
import {
|
||||||
|
type InfiniteData,
|
||||||
|
type QueryClient,
|
||||||
|
type QueryKey,
|
||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
export const RQKEY_ROOT = 'actor-starter-packs'
|
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 = (did?: string) => [RQKEY_ROOT, did]
|
||||||
|
export const RQKEY_WITH_MEMBERSHIP = (did?: string) => [
|
||||||
|
RQKEY_WITH_MEMBERSHIP_ROOT,
|
||||||
|
did,
|
||||||
|
]
|
||||||
|
|
||||||
export function useActorStarterPacksQuery({
|
export function useActorStarterPacksQuery({
|
||||||
did,
|
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({
|
export async function invalidateActorStarterPacksQuery({
|
||||||
queryClient,
|
queryClient,
|
||||||
did,
|
did,
|
||||||
@@ -51,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)})
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {EventStopper} from '#/view/com/util/EventStopper'
|
|||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {Button, ButtonIcon} from '#/components/Button'
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
|
import {StarterPackDialog} from '#/components/dialogs/StarterPackDialog'
|
||||||
import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox'
|
import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox'
|
||||||
import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
|
import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
|
||||||
import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheckIcon} from '#/components/icons/CircleCheck'
|
import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheckIcon} from '#/components/icons/CircleCheck'
|
||||||
@@ -45,6 +46,7 @@ import {
|
|||||||
} from '#/components/icons/Person'
|
} from '#/components/icons/Person'
|
||||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
||||||
|
import {StarterPack} from '#/components/icons/StarterPack'
|
||||||
import {EditLiveDialog} from '#/components/live/EditLiveDialog'
|
import {EditLiveDialog} from '#/components/live/EditLiveDialog'
|
||||||
import {GoLiveDialog} from '#/components/live/GoLiveDialog'
|
import {GoLiveDialog} from '#/components/live/GoLiveDialog'
|
||||||
import * as Menu from '#/components/Menu'
|
import * as Menu from '#/components/Menu'
|
||||||
@@ -88,6 +90,7 @@ let ProfileMenu = ({
|
|||||||
const blockPromptControl = Prompt.usePromptControl()
|
const blockPromptControl = Prompt.usePromptControl()
|
||||||
const loggedOutWarningPromptControl = Prompt.usePromptControl()
|
const loggedOutWarningPromptControl = Prompt.usePromptControl()
|
||||||
const goLiveDialogControl = useDialogControl()
|
const goLiveDialogControl = useDialogControl()
|
||||||
|
const addToStarterPacksDialogControl = useDialogControl()
|
||||||
|
|
||||||
const showLoggedOutWarning = React.useMemo(() => {
|
const showLoggedOutWarning = React.useMemo(() => {
|
||||||
return (
|
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
|
<Menu.Item
|
||||||
testID="profileHeaderDropdownListAddRemoveBtn"
|
testID="profileHeaderDropdownListAddRemoveBtn"
|
||||||
label={_(msg`Add to lists`)}
|
label={_(msg`Add to lists`)}
|
||||||
@@ -440,6 +452,11 @@ let ProfileMenu = ({
|
|||||||
</Menu.Outer>
|
</Menu.Outer>
|
||||||
</Menu.Root>
|
</Menu.Root>
|
||||||
|
|
||||||
|
<StarterPackDialog
|
||||||
|
control={addToStarterPacksDialogControl}
|
||||||
|
targetDid={profile.did}
|
||||||
|
/>
|
||||||
|
|
||||||
<ReportDialog
|
<ReportDialog
|
||||||
control={reportDialogControl}
|
control={reportDialogControl}
|
||||||
subject={{
|
subject={{
|
||||||
|
|||||||
Reference in New Issue
Block a user