tangled
alpha
login
or
join now
quilling.dev
/
social-app
7
fork
atom
An ATproto social media client -- with an independent Appview.
7
fork
atom
overview
issues
pulls
pipelines
starter pack dialog flow from profileMenu
Chenyu Huang
7 months ago
7182cd3d
cced762a
+486
-18
6 changed files
expand all
collapse all
unified
split
src
components
StarterPack
ProfileStarterPacks.tsx
dialogs
StarterPackDialog.tsx
lib
routes
types.ts
screens
StarterPack
Wizard
index.tsx
state
queries
actor-starter-packs.ts
view
com
profile
ProfileMenu.tsx
+3
-3
src/components/StarterPack/ProfileStarterPacks.tsx
···
180
180
color="secondary"
181
181
size="small"
182
182
style={[a.self_center]}
183
183
-
onPress={() => navigation.navigate('StarterPackWizard')}>
183
183
+
onPress={() => navigation.navigate('StarterPackWizard', {})}>
184
184
<ButtonText>
185
185
<Trans>Create another</Trans>
186
186
</ButtonText>
···
238
238
],
239
239
})
240
240
const navToWizard = useCallback(() => {
241
241
-
navigation.navigate('StarterPackWizard')
241
241
+
navigation.navigate('StarterPackWizard', {})
242
242
}, [navigation])
243
243
const wrappedNavToWizard = requireEmailVerification(navToWizard, {
244
244
instructions: [
···
322
322
color="secondary"
323
323
cta={_(msg`Let me choose`)}
324
324
onPress={() => {
325
325
-
navigation.navigate('StarterPackWizard')
325
325
+
navigation.navigate('StarterPackWizard', {})
326
326
}}
327
327
/>
328
328
</Prompt.Actions>
+388
src/components/dialogs/StarterPackDialog.tsx
···
1
1
+
import React from 'react'
2
2
+
import {View} from 'react-native'
3
3
+
import {
4
4
+
type AppBskyGraphGetStarterPacksWithMembership,
5
5
+
AppBskyGraphStarterpack,
6
6
+
} from '@atproto/api'
7
7
+
import {msg, Trans} from '@lingui/macro'
8
8
+
import {useLingui} from '@lingui/react'
9
9
+
import {useNavigation} from '@react-navigation/native'
10
10
+
import {useQueryClient} from '@tanstack/react-query'
11
11
+
12
12
+
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
13
13
+
import {type NavigationProp} from '#/lib/routes/types'
14
14
+
import {
15
15
+
RQKEY_WITH_MEMBERSHIP,
16
16
+
useActorStarterPacksWithMembershipsQuery,
17
17
+
} from '#/state/queries/actor-starter-packs'
18
18
+
import {
19
19
+
useListMembershipAddMutation,
20
20
+
useListMembershipRemoveMutation,
21
21
+
} from '#/state/queries/list-memberships'
22
22
+
import {List} from '#/view/com/util/List'
23
23
+
import * as Toast from '#/view/com/util/Toast'
24
24
+
import {UserAvatar} from '#/view/com/util/UserAvatar'
25
25
+
import {atoms as a, useTheme} from '#/alf'
26
26
+
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
27
27
+
import * as Dialog from '#/components/Dialog'
28
28
+
import {Divider} from '#/components/Divider'
29
29
+
import {Text} from '#/components/Typography'
30
30
+
import * as bsky from '#/types/bsky'
31
31
+
import {PlusLarge_Stroke2_Corner0_Rounded} from '../icons/Plus'
32
32
+
import {StarterPack} from '../icons/StarterPack'
33
33
+
import {TimesLarge_Stroke2_Corner0_Rounded} from '../icons/Times'
34
34
+
35
35
+
type StarterPackWithMembership =
36
36
+
AppBskyGraphGetStarterPacksWithMembership.StarterPackWithMembership
37
37
+
38
38
+
// Simple module-level state for dialog coordination
39
39
+
let dialogCallbacks: {
40
40
+
onSuccess?: () => void
41
41
+
} = {}
42
42
+
43
43
+
export function notifyDialogSuccess() {
44
44
+
if (dialogCallbacks.onSuccess) {
45
45
+
dialogCallbacks.onSuccess()
46
46
+
}
47
47
+
}
48
48
+
49
49
+
export type StarterPackDialogProps = {
50
50
+
control: Dialog.DialogControlProps
51
51
+
accountDid: string
52
52
+
targetDid: string
53
53
+
enabled?: boolean
54
54
+
}
55
55
+
56
56
+
export function StarterPackDialog({
57
57
+
control,
58
58
+
accountDid: _accountDid,
59
59
+
targetDid,
60
60
+
enabled,
61
61
+
}: StarterPackDialogProps) {
62
62
+
const {_} = useLingui()
63
63
+
const navigation = useNavigation<NavigationProp>()
64
64
+
const requireEmailVerification = useRequireEmailVerification()
65
65
+
66
66
+
React.useEffect(() => {
67
67
+
dialogCallbacks.onSuccess = () => {
68
68
+
if (!control.isOpen) {
69
69
+
control.open()
70
70
+
}
71
71
+
}
72
72
+
}, [control])
73
73
+
74
74
+
const navToWizard = React.useCallback(() => {
75
75
+
control.close()
76
76
+
navigation.navigate('StarterPackWizard', {fromDialog: true})
77
77
+
}, [navigation, control])
78
78
+
79
79
+
const wrappedNavToWizard = requireEmailVerification(navToWizard, {
80
80
+
instructions: [
81
81
+
<Trans key="nav">
82
82
+
Before creating a starter pack, you must first verify your email.
83
83
+
</Trans>,
84
84
+
],
85
85
+
})
86
86
+
87
87
+
const onClose = React.useCallback(() => {
88
88
+
// setCurrentView('initial')
89
89
+
control.close()
90
90
+
}, [control])
91
91
+
92
92
+
const t = useTheme()
93
93
+
94
94
+
return (
95
95
+
<Dialog.Outer control={control}>
96
96
+
<Dialog.Handle />
97
97
+
<Dialog.Inner label={_(msg`Add to starter packs`)} style={[a.w_full]}>
98
98
+
<View>
99
99
+
<View
100
100
+
style={[
101
101
+
{justifyContent: 'space-between', flexDirection: 'row'},
102
102
+
a.my_lg,
103
103
+
]}>
104
104
+
<Text style={[a.text_lg, a.font_bold]}>
105
105
+
<Trans>Add to starter packs</Trans>
106
106
+
</Text>
107
107
+
<TimesLarge_Stroke2_Corner0_Rounded
108
108
+
onPress={onClose}
109
109
+
fill={t.atoms.text_contrast_medium.color}
110
110
+
/>
111
111
+
</View>
112
112
+
113
113
+
<StarterPackList
114
114
+
onStartWizard={wrappedNavToWizard}
115
115
+
targetDid={targetDid}
116
116
+
enabled={enabled}
117
117
+
/>
118
118
+
</View>
119
119
+
</Dialog.Inner>
120
120
+
</Dialog.Outer>
121
121
+
)
122
122
+
}
123
123
+
124
124
+
function Empty({onStartWizard}: {onStartWizard: () => void}) {
125
125
+
const {_} = useLingui()
126
126
+
const t = useTheme()
127
127
+
128
128
+
return (
129
129
+
<View
130
130
+
style={[a.align_center, a.gap_2xl, {paddingTop: 64, paddingBottom: 64}]}>
131
131
+
<View style={[a.gap_xs, a.align_center]}>
132
132
+
<StarterPack
133
133
+
width={48}
134
134
+
fill={t.atoms.border_contrast_medium.borderColor}
135
135
+
/>
136
136
+
<Text style={[a.text_center]}>
137
137
+
<Trans>You have no starter packs.</Trans>
138
138
+
</Text>
139
139
+
</View>
140
140
+
141
141
+
<Button
142
142
+
label={_(msg`Create starter pack`)}
143
143
+
color="secondary_inverted"
144
144
+
size="small"
145
145
+
onPress={onStartWizard}>
146
146
+
<ButtonText>
147
147
+
<Trans>Create</Trans>
148
148
+
</ButtonText>
149
149
+
<ButtonIcon icon={PlusLarge_Stroke2_Corner0_Rounded} />
150
150
+
</Button>
151
151
+
</View>
152
152
+
)
153
153
+
}
154
154
+
155
155
+
function StarterPackList({
156
156
+
onStartWizard,
157
157
+
targetDid,
158
158
+
enabled,
159
159
+
}: {
160
160
+
onStartWizard: () => void
161
161
+
targetDid: string
162
162
+
enabled?: boolean
163
163
+
}) {
164
164
+
const {_} = useLingui()
165
165
+
166
166
+
const {
167
167
+
data,
168
168
+
refetch,
169
169
+
isError,
170
170
+
hasNextPage,
171
171
+
isFetchingNextPage,
172
172
+
fetchNextPage,
173
173
+
} = useActorStarterPacksWithMembershipsQuery({did: targetDid, enabled})
174
174
+
175
175
+
const membershipItems =
176
176
+
data?.pages.flatMap(page => page.starterPacksWithMembership) || []
177
177
+
178
178
+
const _onRefresh = React.useCallback(async () => {
179
179
+
try {
180
180
+
await refetch()
181
181
+
} catch (err) {
182
182
+
// Error handling is optional since this is just a refresh
183
183
+
}
184
184
+
}, [refetch])
185
185
+
186
186
+
const _onEndReached = React.useCallback(async () => {
187
187
+
if (isFetchingNextPage || !hasNextPage || isError) return
188
188
+
try {
189
189
+
await fetchNextPage()
190
190
+
} catch (err) {
191
191
+
// Error handling is optional since this is just pagination
192
192
+
}
193
193
+
}, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
194
194
+
195
195
+
const renderItem = React.useCallback(
196
196
+
({item}: {item: StarterPackWithMembership}) => (
197
197
+
<StarterPackItem starterPackWithMembership={item} targetDid={targetDid} />
198
198
+
),
199
199
+
[targetDid],
200
200
+
)
201
201
+
202
202
+
const ListHeaderComponent = React.useCallback(
203
203
+
() => (
204
204
+
<>
205
205
+
<View style={[a.flex_row, a.justify_between, a.align_center, a.py_md]}>
206
206
+
<Text style={[a.text_md, a.font_bold]}>
207
207
+
<Trans>New starter pack</Trans>
208
208
+
</Text>
209
209
+
<Button
210
210
+
label={_(msg`Create starter pack`)}
211
211
+
color="secondary_inverted"
212
212
+
size="small"
213
213
+
onPress={onStartWizard}>
214
214
+
<ButtonText>
215
215
+
<Trans>Create</Trans>
216
216
+
</ButtonText>
217
217
+
<ButtonIcon icon={PlusLarge_Stroke2_Corner0_Rounded} />
218
218
+
</Button>
219
219
+
</View>
220
220
+
<Divider />
221
221
+
</>
222
222
+
),
223
223
+
[_, onStartWizard],
224
224
+
)
225
225
+
226
226
+
return (
227
227
+
<List
228
228
+
data={membershipItems}
229
229
+
renderItem={renderItem}
230
230
+
keyExtractor={(item: StarterPackWithMembership, index: number) =>
231
231
+
item.starterPack.uri || index.toString()
232
232
+
}
233
233
+
refreshing={false}
234
234
+
onRefresh={_onRefresh}
235
235
+
onEndReached={_onEndReached}
236
236
+
onEndReachedThreshold={0.1}
237
237
+
ListHeaderComponent={
238
238
+
membershipItems.length > 0 ? ListHeaderComponent : null
239
239
+
}
240
240
+
ListEmptyComponent={<Empty onStartWizard={onStartWizard} />}
241
241
+
/>
242
242
+
)
243
243
+
}
244
244
+
245
245
+
function StarterPackItem({
246
246
+
starterPackWithMembership,
247
247
+
targetDid,
248
248
+
}: {
249
249
+
starterPackWithMembership: StarterPackWithMembership
250
250
+
targetDid: string
251
251
+
}) {
252
252
+
const {_} = useLingui()
253
253
+
const t = useTheme()
254
254
+
const queryClient = useQueryClient()
255
255
+
const [isUpdating, setIsUpdating] = React.useState(false)
256
256
+
257
257
+
const starterPack = starterPackWithMembership.starterPack
258
258
+
const isInPack = !!starterPackWithMembership.listItem
259
259
+
console.log('StarterPackItem render. 111', {
260
260
+
starterPackWithMembership: starterPackWithMembership.listItem?.subject,
261
261
+
})
262
262
+
263
263
+
console.log('StarterPackItem render', {
264
264
+
starterPackWithMembership,
265
265
+
})
266
266
+
267
267
+
const {mutateAsync: addMembership} = useListMembershipAddMutation({
268
268
+
onSuccess: () => {
269
269
+
Toast.show(_(msg`Added to starter pack`))
270
270
+
},
271
271
+
onError: () => {
272
272
+
Toast.show(_(msg`Failed to add to starter pack`), 'xmark')
273
273
+
},
274
274
+
})
275
275
+
276
276
+
const {mutateAsync: removeMembership} = useListMembershipRemoveMutation({
277
277
+
onSuccess: () => {
278
278
+
Toast.show(_(msg`Removed from starter pack`))
279
279
+
},
280
280
+
onError: () => {
281
281
+
Toast.show(_(msg`Failed to remove from starter pack`), 'xmark')
282
282
+
},
283
283
+
})
284
284
+
285
285
+
const handleToggleMembership = async () => {
286
286
+
if (!starterPack.list?.uri || isUpdating) return
287
287
+
288
288
+
const listUri = starterPack.list.uri
289
289
+
setIsUpdating(true)
290
290
+
291
291
+
try {
292
292
+
if (!isInPack) {
293
293
+
await addMembership({
294
294
+
listUri: listUri,
295
295
+
actorDid: targetDid,
296
296
+
})
297
297
+
} else {
298
298
+
if (!starterPackWithMembership.listItem?.uri) {
299
299
+
console.error('Cannot remove: missing membership URI')
300
300
+
return
301
301
+
}
302
302
+
await removeMembership({
303
303
+
listUri: listUri,
304
304
+
actorDid: targetDid,
305
305
+
membershipUri: starterPackWithMembership.listItem.uri,
306
306
+
})
307
307
+
}
308
308
+
309
309
+
await Promise.all([
310
310
+
queryClient.invalidateQueries({
311
311
+
queryKey: RQKEY_WITH_MEMBERSHIP(targetDid),
312
312
+
}),
313
313
+
])
314
314
+
} catch (error) {
315
315
+
console.error('Failed to toggle membership:', error)
316
316
+
} finally {
317
317
+
setIsUpdating(false)
318
318
+
}
319
319
+
}
320
320
+
321
321
+
const {record} = starterPack
322
322
+
323
323
+
if (
324
324
+
!bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
325
325
+
record,
326
326
+
AppBskyGraphStarterpack.isRecord,
327
327
+
)
328
328
+
) {
329
329
+
return null
330
330
+
}
331
331
+
332
332
+
return (
333
333
+
<View style={[a.flex_row, a.justify_between, a.align_center, a.py_md]}>
334
334
+
<View>
335
335
+
<Text emoji style={[a.text_md, a.font_bold]} numberOfLines={1}>
336
336
+
{record.name}
337
337
+
</Text>
338
338
+
339
339
+
<View style={[a.flex_row, a.align_center, a.mt_xs]}>
340
340
+
{starterPack.listItemsSample &&
341
341
+
starterPack.listItemsSample.length > 0 && (
342
342
+
<>
343
343
+
{starterPack.listItemsSample?.slice(0, 4).map((p, index) => (
344
344
+
<UserAvatar
345
345
+
key={p.subject.did}
346
346
+
avatar={p.subject.avatar}
347
347
+
size={32}
348
348
+
type={'user'}
349
349
+
style={[
350
350
+
{
351
351
+
zIndex: 1 - index,
352
352
+
marginLeft: index > 0 ? -2 : 0,
353
353
+
borderWidth: 0.5,
354
354
+
borderColor: t.atoms.bg.backgroundColor,
355
355
+
},
356
356
+
]}
357
357
+
/>
358
358
+
))}
359
359
+
360
360
+
{starterPack.list?.listItemCount &&
361
361
+
starterPack.list.listItemCount > 4 && (
362
362
+
<Text
363
363
+
style={[
364
364
+
a.text_sm,
365
365
+
t.atoms.text_contrast_medium,
366
366
+
a.ml_xs,
367
367
+
]}>
368
368
+
{`+${starterPack.list.listItemCount - 4} more`}
369
369
+
</Text>
370
370
+
)}
371
371
+
</>
372
372
+
)}
373
373
+
</View>
374
374
+
</View>
375
375
+
376
376
+
<Button
377
377
+
label={isInPack ? _(msg`Remove`) : _(msg`Add`)}
378
378
+
color={isInPack ? 'secondary' : 'primary'}
379
379
+
size="tiny"
380
380
+
disabled={isUpdating}
381
381
+
onPress={handleToggleMembership}>
382
382
+
<ButtonText>
383
383
+
{isInPack ? <Trans>Remove</Trans> : <Trans>Add</Trans>}
384
384
+
</ButtonText>
385
385
+
</Button>
386
386
+
</View>
387
387
+
)
388
388
+
}
+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
82
-
StarterPackWizard: undefined
82
82
+
StarterPackWizard: {fromDialog?: boolean}
83
83
StarterPackEdit: {rkey?: string}
84
84
VideoFeed: VideoFeedSourceContext
85
85
}
+31
-10
src/screens/StarterPack/Wizard/index.tsx
···
54
54
import {atoms as a, useTheme, web} from '#/alf'
55
55
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
56
56
import {useDialogControl} from '#/components/Dialog'
57
57
+
import {notifyDialogSuccess} from '#/components/dialogs/StarterPackDialog'
57
58
import * as Layout from '#/components/Layout'
58
59
import {ListMaybePlaceholder} from '#/components/Lists'
59
60
import {Loader} from '#/components/Loader'
···
68
69
CommonNavigatorParams,
69
70
'StarterPackEdit' | 'StarterPackWizard'
70
71
>) {
71
71
-
const {rkey} = route.params ?? {}
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
72
75
const {currentAccount} = useSession()
73
76
const moderationOpts = useModerationOpts()
74
77
···
133
136
currentListItems={listItems}
134
137
profile={profile}
135
138
moderationOpts={moderationOpts}
139
139
+
fromDialog={fromDialog}
136
140
/>
137
141
</Provider>
138
142
</Layout.Screen>
···
144
148
currentListItems,
145
149
profile,
146
150
moderationOpts,
151
151
+
fromDialog,
147
152
}: {
148
153
currentStarterPack?: AppBskyGraphDefs.StarterPackView
149
154
currentListItems?: AppBskyGraphDefs.ListItemView[]
150
155
profile: AppBskyActorDefs.ProfileViewDetailed
151
156
moderationOpts: ModerationOpts
157
157
+
fromDialog?: boolean
152
158
}) {
153
159
const navigation = useNavigation<NavigationProp>()
154
160
const {_} = useLingui()
155
161
const setMinimalShellMode = useSetMinimalShellMode()
156
162
const [state, dispatch] = useWizardState()
157
163
const {currentAccount} = useSession()
164
164
+
158
165
const {data: currentProfile} = useProfileQuery({
159
166
did: currentAccount?.did,
160
167
staleTime: 0,
···
213
220
})
214
221
Image.prefetch([getStarterPackOgCard(currentProfile!.did, rkey)])
215
222
dispatch({type: 'SetProcessing', processing: false})
216
216
-
navigation.replace('StarterPack', {
217
217
-
name: currentAccount!.handle,
218
218
-
rkey,
219
219
-
new: true,
220
220
-
})
221
221
-
}
222
223
223
223
-
const onSuccessEdit = () => {
224
224
-
if (navigation.canGoBack()) {
224
224
+
// If launched from ProfileMenu dialog, notify the dialog and go back
225
225
+
if (fromDialog) {
225
226
navigation.goBack()
227
227
+
notifyDialogSuccess()
226
228
} else {
229
229
+
// Original behavior for other entry points
227
230
navigation.replace('StarterPack', {
228
231
name: currentAccount!.handle,
229
229
-
rkey: parsed!.rkey,
232
232
+
rkey,
233
233
+
new: true,
230
234
})
235
235
+
}
236
236
+
}
237
237
+
238
238
+
const onSuccessEdit = () => {
239
239
+
// If launched from ProfileMenu dialog, go back to stay on profile page
240
240
+
if (fromDialog) {
241
241
+
navigation.goBack()
242
242
+
} else {
243
243
+
// Original behavior for other entry points
244
244
+
if (navigation.canGoBack()) {
245
245
+
navigation.goBack()
246
246
+
} else {
247
247
+
navigation.replace('StarterPack', {
248
248
+
name: currentAccount!.handle,
249
249
+
rkey: parsed!.rkey,
250
250
+
})
251
251
+
}
231
252
}
232
253
}
233
254
+43
-4
src/state/queries/actor-starter-packs.ts
···
1
1
-
import {AppBskyGraphGetActorStarterPacks} from '@atproto/api'
2
1
import {
3
3
-
InfiniteData,
4
4
-
QueryClient,
5
5
-
QueryKey,
2
2
+
type AppBskyGraphGetActorStarterPacks,
3
3
+
type AppBskyGraphGetStarterPacksWithMembership,
4
4
+
} from '@atproto/api'
5
5
+
import {
6
6
+
type InfiniteData,
7
7
+
type QueryClient,
8
8
+
type QueryKey,
6
9
useInfiniteQuery,
7
10
} from '@tanstack/react-query'
8
11
9
12
import {useAgent} from '#/state/session'
10
13
11
14
export const RQKEY_ROOT = 'actor-starter-packs'
15
15
+
export const RQKEY_WITH_MEMBERSHIP_ROOT = 'actor-starter-packs-with-membership'
12
16
export const RQKEY = (did?: string) => [RQKEY_ROOT, did]
17
17
+
export const RQKEY_WITH_MEMBERSHIP = (did?: string) => [
18
18
+
RQKEY_WITH_MEMBERSHIP_ROOT,
19
19
+
did,
20
20
+
]
13
21
14
22
export function useActorStarterPacksQuery({
15
23
did,
···
30
38
queryKey: RQKEY(did),
31
39
queryFn: async ({pageParam}: {pageParam?: string}) => {
32
40
const res = await agent.app.bsky.graph.getActorStarterPacks({
41
41
+
actor: did!,
42
42
+
limit: 10,
43
43
+
cursor: pageParam,
44
44
+
})
45
45
+
return res.data
46
46
+
},
47
47
+
enabled: Boolean(did) && enabled,
48
48
+
initialPageParam: undefined,
49
49
+
getNextPageParam: lastPage => lastPage.cursor,
50
50
+
})
51
51
+
}
52
52
+
53
53
+
export function useActorStarterPacksWithMembershipsQuery({
54
54
+
did,
55
55
+
enabled = true,
56
56
+
}: {
57
57
+
did?: string
58
58
+
enabled?: boolean
59
59
+
}) {
60
60
+
const agent = useAgent()
61
61
+
62
62
+
return useInfiniteQuery<
63
63
+
AppBskyGraphGetStarterPacksWithMembership.OutputSchema,
64
64
+
Error,
65
65
+
InfiniteData<AppBskyGraphGetStarterPacksWithMembership.OutputSchema>,
66
66
+
QueryKey,
67
67
+
string | undefined
68
68
+
>({
69
69
+
queryKey: RQKEY_WITH_MEMBERSHIP(did),
70
70
+
queryFn: async ({pageParam}: {pageParam?: string}) => {
71
71
+
const res = await agent.app.bsky.graph.getStarterPacksWithMembership({
33
72
actor: did!,
34
73
limit: 10,
35
74
cursor: pageParam,
+20
src/view/com/profile/ProfileMenu.tsx
···
27
27
import * as Toast from '#/view/com/util/Toast'
28
28
import {Button, ButtonIcon} from '#/components/Button'
29
29
import {useDialogControl} from '#/components/Dialog'
30
30
+
import {StarterPackDialog} from '#/components/dialogs/StarterPackDialog'
30
31
import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox'
31
32
import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
32
33
import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheckIcon} from '#/components/icons/CircleCheck'
···
45
46
} from '#/components/icons/Person'
46
47
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
47
48
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
49
49
+
import {StarterPack} from '#/components/icons/StarterPack'
48
50
import {EditLiveDialog} from '#/components/live/EditLiveDialog'
49
51
import {GoLiveDialog} from '#/components/live/GoLiveDialog'
50
52
import * as Menu from '#/components/Menu'
···
88
90
const blockPromptControl = Prompt.usePromptControl()
89
91
const loggedOutWarningPromptControl = Prompt.usePromptControl()
90
92
const goLiveDialogControl = useDialogControl()
93
93
+
const addToStarterPacksDialogControl = useDialogControl()
91
94
92
95
const showLoggedOutWarning = React.useMemo(() => {
93
96
return (
···
301
304
</>
302
305
)}
303
306
<Menu.Item
307
307
+
testID="profileHeaderDropdownStarterPackAddRemoveBtn"
308
308
+
label={_(msg`Add to starter packs`)}
309
309
+
onPress={addToStarterPacksDialogControl.open}>
310
310
+
<Menu.ItemText>
311
311
+
<Trans>Add to starter packs</Trans>
312
312
+
</Menu.ItemText>
313
313
+
<Menu.ItemIcon icon={StarterPack} />
314
314
+
</Menu.Item>
315
315
+
<Menu.Item
304
316
testID="profileHeaderDropdownListAddRemoveBtn"
305
317
label={_(msg`Add to lists`)}
306
318
onPress={onPressAddRemoveLists}>
···
439
451
) : null}
440
452
</Menu.Outer>
441
453
</Menu.Root>
454
454
+
455
455
+
{currentAccount && (
456
456
+
<StarterPackDialog
457
457
+
control={addToStarterPacksDialogControl}
458
458
+
accountDid={currentAccount.did}
459
459
+
targetDid={profile.did}
460
460
+
/>
461
461
+
)}
442
462
443
463
<ReportDialog
444
464
control={reportDialogControl}