forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import React from 'react'
2import {View} from 'react-native'
3import {Image} from 'expo-image'
4import {
5 AppBskyGraphDefs,
6 AppBskyGraphStarterpack,
7 AtUri,
8 type ModerationOpts,
9 RichText as RichTextAPI,
10} from '@atproto/api'
11import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
12import {msg} from '@lingui/core/macro'
13import {useLingui} from '@lingui/react'
14import {Plural, Trans} from '@lingui/react/macro'
15import {useNavigation} from '@react-navigation/native'
16import {type NativeStackScreenProps} from '@react-navigation/native-stack'
17import {useQueryClient} from '@tanstack/react-query'
18
19import {batchedUpdates} from '#/lib/batchedUpdates'
20import {HITSLOP_20} from '#/lib/constants'
21import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
22import {makeProfileLink, makeStarterPackLink} from '#/lib/routes/links'
23import {
24 type CommonNavigatorParams,
25 type NavigationProp,
26} from '#/lib/routes/types'
27import {cleanError} from '#/lib/strings/errors'
28import {getStarterPackOgCard} from '#/lib/strings/starter-pack'
29import {logger} from '#/logger'
30import {updateProfileShadow} from '#/state/cache/profile-shadow'
31import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
32import {useModerationOpts} from '#/state/preferences/moderation-opts'
33import {getAllListMembers} from '#/state/queries/list-members'
34import {useResolvedStarterPackShortLink} from '#/state/queries/resolve-short-link'
35import {useResolveDidQuery} from '#/state/queries/resolve-uri'
36import {useShortenLink} from '#/state/queries/shorten-link'
37import {
38 useDeleteStarterPackMutation,
39 useStarterPackQuery,
40} from '#/state/queries/starter-packs'
41import {useAgent, useSession} from '#/state/session'
42import {useLoggedOutViewControls} from '#/state/shell/logged-out'
43import {
44 ProgressGuideAction,
45 useProgressGuideControls,
46} from '#/state/shell/progress-guide'
47import {useSetActiveStarterPack} from '#/state/shell/starter-pack'
48import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
49import {ProfileSubpageHeader} from '#/view/com/profile/ProfileSubpageHeader'
50import * as Toast from '#/view/com/util/Toast'
51import {bulkWriteFollows} from '#/screens/Onboarding/util'
52import {atoms as a, useBreakpoints, useTheme} from '#/alf'
53import {Button, ButtonIcon, ButtonText} from '#/components/Button'
54import {useDialogControl} from '#/components/Dialog'
55import {CreateListFromStarterPackDialog} from '#/components/dialogs/lists/CreateListFromStarterPackDialog'
56import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox'
57import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
58import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
59import {DotGrid3x1_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid'
60import {ListSparkle_Stroke2_Corner0_Rounded as ListSparkle} from '#/components/icons/ListSparkle'
61import {Pencil_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil'
62import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
63import * as Layout from '#/components/Layout'
64import {ListMaybePlaceholder} from '#/components/Lists'
65import {Loader} from '#/components/Loader'
66import * as Menu from '#/components/Menu'
67import {
68 ReportDialog,
69 useReportDialogControl,
70} from '#/components/moderation/ReportDialog'
71import * as Prompt from '#/components/Prompt'
72import {RichText} from '#/components/RichText'
73import {FeedsList} from '#/components/StarterPack/Main/FeedsList'
74import {PostsList} from '#/components/StarterPack/Main/PostsList'
75import {ProfilesList} from '#/components/StarterPack/Main/ProfilesList'
76import {QrCodeDialog} from '#/components/StarterPack/QrCodeDialog'
77import {ShareDialog} from '#/components/StarterPack/ShareDialog'
78import {Text} from '#/components/Typography'
79import {useAnalytics} from '#/analytics'
80import {IS_WEB} from '#/env'
81import * as bsky from '#/types/bsky'
82
83type StarterPackScreeProps = NativeStackScreenProps<
84 CommonNavigatorParams,
85 'StarterPack'
86>
87type StarterPackScreenShortProps = NativeStackScreenProps<
88 CommonNavigatorParams,
89 'StarterPackShort'
90>
91
92export function StarterPackScreen({route}: StarterPackScreeProps) {
93 return (
94 <Layout.Screen>
95 <StarterPackScreenInner routeParams={route.params} />
96 </Layout.Screen>
97 )
98}
99
100export function StarterPackScreenShort({route}: StarterPackScreenShortProps) {
101 const {_} = useLingui()
102 const {
103 data: resolvedStarterPack,
104 isLoading,
105 isError,
106 } = useResolvedStarterPackShortLink({
107 code: route.params.code,
108 })
109
110 if (isLoading || isError || !resolvedStarterPack) {
111 return (
112 <Layout.Screen>
113 <ListMaybePlaceholder
114 isLoading={isLoading}
115 isError={isError}
116 errorMessage={_(msg`That starter pack could not be found.`)}
117 emptyMessage={_(msg`That starter pack could not be found.`)}
118 />
119 </Layout.Screen>
120 )
121 }
122 return (
123 <Layout.Screen>
124 <StarterPackScreenInner routeParams={resolvedStarterPack} />
125 </Layout.Screen>
126 )
127}
128
129export function StarterPackScreenInner({
130 routeParams,
131}: {
132 routeParams: StarterPackScreeProps['route']['params']
133}) {
134 const {name, rkey} = routeParams
135 const {_} = useLingui()
136 const {currentAccount} = useSession()
137
138 const moderationOpts = useModerationOpts()
139 const {
140 data: did,
141 isLoading: isLoadingDid,
142 isError: isErrorDid,
143 } = useResolveDidQuery(name)
144 const {
145 data: starterPack,
146 isLoading: isLoadingStarterPack,
147 isError: isErrorStarterPack,
148 } = useStarterPackQuery({did, rkey})
149
150 const isValid =
151 starterPack &&
152 (starterPack.list || starterPack?.creator?.did === currentAccount?.did) &&
153 AppBskyGraphDefs.validateStarterPackView(starterPack) &&
154 AppBskyGraphStarterpack.validateRecord(starterPack.record)
155
156 if (!did || !starterPack || !isValid || !moderationOpts) {
157 return (
158 <ListMaybePlaceholder
159 isLoading={isLoadingDid || isLoadingStarterPack || !moderationOpts}
160 isError={isErrorDid || isErrorStarterPack || !isValid}
161 errorMessage={_(msg`That starter pack could not be found.`)}
162 emptyMessage={_(msg`That starter pack could not be found.`)}
163 />
164 )
165 }
166
167 if (!starterPack.list && starterPack.creator.did === currentAccount?.did) {
168 return <InvalidStarterPack rkey={rkey} />
169 }
170
171 return (
172 <StarterPackScreenLoaded
173 starterPack={starterPack}
174 routeParams={routeParams}
175 moderationOpts={moderationOpts}
176 />
177 )
178}
179
180function StarterPackScreenLoaded({
181 starterPack,
182 routeParams,
183 moderationOpts,
184}: {
185 starterPack: AppBskyGraphDefs.StarterPackView
186 routeParams: StarterPackScreeProps['route']['params']
187 moderationOpts: ModerationOpts
188}) {
189 const showPeopleTab = Boolean(starterPack.list)
190 const showFeedsTab = Boolean(starterPack.feeds?.length)
191 const showPostsTab = Boolean(starterPack.list)
192 const {_} = useLingui()
193 const ax = useAnalytics()
194
195 const tabs = [
196 ...(showPeopleTab ? [_(msg`People`)] : []),
197 ...(showFeedsTab ? [_(msg`Feeds`)] : []),
198 ...(showPostsTab ? [_(msg`Posts`)] : []),
199 ]
200
201 const qrCodeDialogControl = useDialogControl()
202 const shareDialogControl = useDialogControl()
203
204 const shortenLink = useShortenLink()
205 const [link, setLink] = React.useState<string>()
206 const [imageLoaded, setImageLoaded] = React.useState(false)
207
208 React.useEffect(() => {
209 ax.metric('starterPack:opened', {
210 starterPack: starterPack.uri,
211 })
212 }, [ax, starterPack.uri])
213
214 const onOpenShareDialog = React.useCallback(() => {
215 const rkey = new AtUri(starterPack.uri).rkey
216 shortenLink(makeStarterPackLink(starterPack.creator.did, rkey)).then(
217 res => {
218 setLink(res.url)
219 },
220 )
221 Image.prefetch(getStarterPackOgCard(starterPack))
222 .then(() => {
223 setImageLoaded(true)
224 })
225 .catch(() => {
226 setImageLoaded(true)
227 })
228 shareDialogControl.open()
229 }, [shareDialogControl, shortenLink, starterPack])
230
231 React.useEffect(() => {
232 if (routeParams.new) {
233 onOpenShareDialog()
234 }
235 }, [onOpenShareDialog, routeParams.new, shareDialogControl])
236
237 return (
238 <>
239 <PagerWithHeader
240 items={tabs}
241 isHeaderReady={true}
242 renderHeader={() => (
243 <Header
244 starterPack={starterPack}
245 routeParams={routeParams}
246 onOpenShareDialog={onOpenShareDialog}
247 />
248 )}>
249 {showPeopleTab
250 ? ({headerHeight, scrollElRef}) => (
251 <ProfilesList
252 // Validated above
253 listUri={starterPack.list!.uri}
254 headerHeight={headerHeight}
255 // @ts-expect-error
256 scrollElRef={scrollElRef}
257 moderationOpts={moderationOpts}
258 />
259 )
260 : null}
261 {showFeedsTab
262 ? ({headerHeight, scrollElRef}) => (
263 <FeedsList
264 // @ts-expect-error ?
265 feeds={starterPack?.feeds}
266 headerHeight={headerHeight}
267 // @ts-expect-error
268 scrollElRef={scrollElRef}
269 />
270 )
271 : null}
272 {showPostsTab
273 ? ({headerHeight, scrollElRef}) => (
274 <PostsList
275 // Validated above
276 listUri={starterPack.list!.uri}
277 headerHeight={headerHeight}
278 // @ts-expect-error
279 scrollElRef={scrollElRef}
280 moderationOpts={moderationOpts}
281 />
282 )
283 : null}
284 </PagerWithHeader>
285
286 <QrCodeDialog
287 control={qrCodeDialogControl}
288 starterPack={starterPack}
289 link={link}
290 />
291 <ShareDialog
292 control={shareDialogControl}
293 qrDialogControl={qrCodeDialogControl}
294 starterPack={starterPack}
295 link={link}
296 imageLoaded={imageLoaded}
297 />
298 </>
299 )
300}
301
302function Header({
303 starterPack,
304 routeParams,
305 onOpenShareDialog,
306}: {
307 starterPack: AppBskyGraphDefs.StarterPackView
308 routeParams: StarterPackScreeProps['route']['params']
309 onOpenShareDialog: () => void
310}) {
311 const {_} = useLingui()
312 const t = useTheme()
313 const {currentAccount, hasSession} = useSession()
314 const agent = useAgent()
315 const queryClient = useQueryClient()
316 const setActiveStarterPack = useSetActiveStarterPack()
317 const {requestSwitchToAccount} = useLoggedOutViewControls()
318 const {captureAction} = useProgressGuideControls()
319
320 const [isProcessing, setIsProcessing] = React.useState(false)
321
322 const {record, creator} = starterPack
323 const isOwn = creator?.did === currentAccount?.did
324 const joinedAllTimeCount = starterPack.joinedAllTimeCount ?? 0
325 const ax = useAnalytics()
326
327 const navigation = useNavigation<NavigationProp>()
328
329 React.useEffect(() => {
330 const onFocus = () => {
331 if (hasSession) return
332 setActiveStarterPack({
333 uri: starterPack.uri,
334 })
335 }
336 const onBeforeRemove = () => {
337 if (hasSession) return
338 setActiveStarterPack(undefined)
339 }
340
341 navigation.addListener('focus', onFocus)
342 navigation.addListener('beforeRemove', onBeforeRemove)
343
344 return () => {
345 navigation.removeListener('focus', onFocus)
346 navigation.removeListener('beforeRemove', onBeforeRemove)
347 }
348 }, [hasSession, navigation, setActiveStarterPack, starterPack.uri])
349
350 const onFollowAll = async () => {
351 if (!starterPack.list) return
352
353 setIsProcessing(true)
354
355 let listItems: AppBskyGraphDefs.ListItemView[] = []
356 try {
357 listItems = await getAllListMembers(agent, starterPack.list.uri)
358 } catch (e) {
359 setIsProcessing(false)
360 Toast.show(_(msg`An error occurred while trying to follow all`), 'xmark')
361 logger.error('Failed to get list members for starter pack', {
362 safeMessage: e,
363 })
364 return
365 }
366
367 const dids = listItems
368 .filter(
369 li =>
370 li.subject.did !== currentAccount?.did &&
371 !isBlockedOrBlocking(li.subject) &&
372 !isMuted(li.subject) &&
373 !li.subject.viewer?.following,
374 )
375 .map(li => li.subject.did)
376
377 let followUris: Map<string, string>
378 try {
379 followUris = await bulkWriteFollows(agent, dids, {
380 uri: starterPack.uri,
381 cid: starterPack.cid,
382 })
383 } catch (e) {
384 setIsProcessing(false)
385 Toast.show(_(msg`An error occurred while trying to follow all`), 'xmark')
386 logger.error('Failed to follow all accounts', {safeMessage: e})
387 }
388
389 setIsProcessing(false)
390 batchedUpdates(() => {
391 for (let did of dids) {
392 updateProfileShadow(queryClient, did, {
393 followingUri: followUris.get(did),
394 })
395 }
396 })
397 Toast.show(_(msg`All accounts have been followed!`))
398 captureAction(ProgressGuideAction.Follow, dids.length)
399 ax.metric('starterPack:followAll', {
400 logContext: 'StarterPackProfilesList',
401 starterPack: starterPack.uri,
402 count: dids.length,
403 })
404 }
405
406 if (
407 !bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
408 record,
409 AppBskyGraphStarterpack.isRecord,
410 )
411 ) {
412 return null
413 }
414
415 const richText = record.description
416 ? new RichTextAPI({
417 text: record.description,
418 facets: record.descriptionFacets,
419 })
420 : undefined
421
422 return (
423 <>
424 <ProfileSubpageHeader
425 isLoading={false}
426 href={makeProfileLink(creator)}
427 title={record.name}
428 isOwner={isOwn}
429 avatar={undefined}
430 creator={creator}
431 purpose="app.bsky.graph.defs#referencelist"
432 avatarType="starter-pack">
433 {hasSession ? (
434 <View style={[a.flex_row, a.gap_sm, a.align_center]}>
435 {isOwn ? (
436 <Button
437 label={_(msg`Share this starter pack`)}
438 hitSlop={HITSLOP_20}
439 variant="solid"
440 color="primary"
441 size="small"
442 onPress={onOpenShareDialog}>
443 <ButtonText>
444 <Trans>Share</Trans>
445 </ButtonText>
446 </Button>
447 ) : (
448 <Button
449 label={_(msg`Follow all`)}
450 variant="solid"
451 color="primary"
452 size="small"
453 disabled={isProcessing}
454 onPress={onFollowAll}
455 style={[a.flex_row, a.gap_xs, a.align_center]}>
456 <ButtonText>
457 <Trans>Follow all</Trans>
458 </ButtonText>
459 {isProcessing && <ButtonIcon icon={Loader} />}
460 </Button>
461 )}
462 <OverflowMenu
463 routeParams={routeParams}
464 starterPack={starterPack}
465 onOpenShareDialog={onOpenShareDialog}
466 />
467 </View>
468 ) : null}
469 </ProfileSubpageHeader>
470 {!hasSession || richText || joinedAllTimeCount >= 25 ? (
471 <View style={[a.px_lg, a.pt_md, a.pb_sm, a.gap_md]}>
472 {richText ? <RichText value={richText} style={[a.text_md]} /> : null}
473 {!hasSession ? (
474 <Button
475 label={_(msg`Join Bluesky`)}
476 onPress={() => {
477 setActiveStarterPack({
478 uri: starterPack.uri,
479 })
480 requestSwitchToAccount({requestedAccount: 'new'})
481 }}
482 variant="solid"
483 color="primary"
484 size="large">
485 <ButtonText style={[a.text_lg]}>
486 <Trans>Join Bluesky</Trans>
487 </ButtonText>
488 </Button>
489 ) : null}
490 {joinedAllTimeCount >= 25 ? (
491 <View style={[a.flex_row, a.align_center, a.gap_sm]}>
492 <FontAwesomeIcon
493 icon="arrow-trend-up"
494 size={12}
495 color={t.atoms.text_contrast_medium.color}
496 />
497 <Text
498 style={[
499 a.font_semi_bold,
500 a.text_sm,
501 t.atoms.text_contrast_medium,
502 ]}>
503 <Trans comment="Number of users (always at least 25) who have joined Bluesky using a specific starter pack">
504 <Plural
505 value={starterPack.joinedAllTimeCount || 0}
506 other="# people have"
507 />{' '}
508 joined Bluesky via this starter pack!
509 </Trans>
510 </Text>
511 </View>
512 ) : null}
513 </View>
514 ) : null}
515 </>
516 )
517}
518
519function OverflowMenu({
520 starterPack,
521 routeParams,
522 onOpenShareDialog,
523}: {
524 starterPack: AppBskyGraphDefs.StarterPackView
525 routeParams: StarterPackScreeProps['route']['params']
526 onOpenShareDialog: () => void
527}) {
528 const t = useTheme()
529 const {_} = useLingui()
530 const ax = useAnalytics()
531 const {gtMobile} = useBreakpoints()
532 const {currentAccount} = useSession()
533 const reportDialogControl = useReportDialogControl()
534 const deleteDialogControl = useDialogControl()
535 const convertToListDialogControl = useDialogControl()
536 const navigation = useNavigation<NavigationProp>()
537
538 const enableSquareButtons = useEnableSquareButtons()
539
540 const {
541 mutate: deleteStarterPack,
542 isPending: isDeletePending,
543 error: deleteError,
544 } = useDeleteStarterPackMutation({
545 onSuccess: () => {
546 ax.metric('starterPack:delete', {})
547 deleteDialogControl.close(() => {
548 if (navigation.canGoBack()) {
549 navigation.popToTop()
550 } else {
551 navigation.navigate('Home')
552 }
553 })
554 },
555 onError: e => {
556 logger.error('Failed to delete starter pack', {safeMessage: e})
557 },
558 })
559
560 const isOwn = starterPack.creator.did === currentAccount?.did
561
562 const onDeleteStarterPack = async () => {
563 if (!starterPack.list) {
564 logger.error(`Unable to delete starterpack because list is missing`)
565 return
566 }
567
568 deleteStarterPack({
569 rkey: routeParams.rkey,
570 listUri: starterPack.list.uri,
571 })
572 ax.metric('starterPack:delete', {})
573 }
574
575 return (
576 <>
577 <Menu.Root>
578 <Menu.Trigger label={_(msg`Repost or quote post`)}>
579 {({props}) => (
580 <Button
581 {...props}
582 testID="headerDropdownBtn"
583 label={_(msg`Open starter pack menu`)}
584 hitSlop={HITSLOP_20}
585 variant="solid"
586 color="secondary"
587 size="small"
588 shape={enableSquareButtons ? 'square' : 'round'}>
589 <ButtonIcon icon={Ellipsis} />
590 </Button>
591 )}
592 </Menu.Trigger>
593 <Menu.Outer style={{minWidth: 170}}>
594 {isOwn ? (
595 <>
596 <Menu.Item
597 label={_(msg`Edit starter pack`)}
598 testID="editStarterPackLinkBtn"
599 onPress={() => {
600 navigation.navigate('StarterPackEdit', {
601 rkey: routeParams.rkey,
602 })
603 }}>
604 <Menu.ItemText>
605 <Trans>Edit</Trans>
606 </Menu.ItemText>
607 <Menu.ItemIcon icon={Pencil} position="right" />
608 </Menu.Item>
609 <Menu.Item
610 label={_(msg`Delete starter pack`)}
611 testID="deleteStarterPackBtn"
612 onPress={() => {
613 deleteDialogControl.open()
614 }}>
615 <Menu.ItemText>
616 <Trans>Delete</Trans>
617 </Menu.ItemText>
618 <Menu.ItemIcon icon={Trash} position="right" />
619 </Menu.Item>
620 <Menu.Item
621 label={_(msg`Create a list from this starter pack`)}
622 testID="convertToListBtn"
623 onPress={() => {
624 convertToListDialogControl.open()
625 }}>
626 <Menu.ItemText>
627 <Trans>Create list from members</Trans>
628 </Menu.ItemText>
629 <Menu.ItemIcon icon={ListSparkle} position="right" />
630 </Menu.Item>
631 </>
632 ) : (
633 <>
634 <Menu.Group>
635 <Menu.Item
636 label={
637 IS_WEB
638 ? _(msg`Copy link to starter pack`)
639 : _(msg`Share via...`)
640 }
641 testID="shareStarterPackLinkBtn"
642 onPress={onOpenShareDialog}>
643 <Menu.ItemText>
644 {IS_WEB ? (
645 <Trans>Copy link</Trans>
646 ) : (
647 <Trans>Share via...</Trans>
648 )}
649 </Menu.ItemText>
650 <Menu.ItemIcon
651 icon={IS_WEB ? ChainLinkIcon : ArrowOutOfBoxIcon}
652 position="right"
653 />
654 </Menu.Item>
655 </Menu.Group>
656
657 <Menu.Item
658 label={_(msg`Report starter pack`)}
659 onPress={() => reportDialogControl.open()}>
660 <Menu.ItemText>
661 <Trans>Report starter pack</Trans>
662 </Menu.ItemText>
663 <Menu.ItemIcon icon={CircleInfo} position="right" />
664 </Menu.Item>
665 </>
666 )}
667 </Menu.Outer>
668 </Menu.Root>
669
670 {starterPack.list && (
671 <ReportDialog
672 control={reportDialogControl}
673 subject={{
674 ...starterPack,
675 $type: 'app.bsky.graph.defs#starterPackView',
676 }}
677 />
678 )}
679
680 <Prompt.Outer control={deleteDialogControl}>
681 <Prompt.TitleText>
682 <Trans>Delete starter pack?</Trans>
683 </Prompt.TitleText>
684 <Prompt.DescriptionText>
685 <Trans>Are you sure you want to delete this starter pack?</Trans>
686 </Prompt.DescriptionText>
687 {deleteError && (
688 <View
689 style={[
690 a.flex_row,
691 a.gap_sm,
692 a.rounded_sm,
693 a.p_md,
694 a.mb_lg,
695 a.border,
696 t.atoms.border_contrast_medium,
697 t.atoms.bg_contrast_25,
698 ]}>
699 <View style={[a.flex_1, a.gap_2xs]}>
700 <Text style={[a.font_semi_bold]}>
701 <Trans>Unable to delete</Trans>
702 </Text>
703 <Text style={[a.leading_snug]}>{cleanError(deleteError)}</Text>
704 </View>
705 <CircleInfo size="sm" fill={t.palette.negative_400} />
706 </View>
707 )}
708 <Prompt.Actions>
709 <Button
710 variant="solid"
711 color="negative"
712 size={gtMobile ? 'small' : 'large'}
713 label={_(msg`Yes, delete this starter pack`)}
714 onPress={onDeleteStarterPack}>
715 <ButtonText>
716 <Trans>Delete</Trans>
717 </ButtonText>
718 {isDeletePending && <ButtonIcon icon={Loader} />}
719 </Button>
720 <Prompt.Cancel />
721 </Prompt.Actions>
722 </Prompt.Outer>
723
724 <CreateListFromStarterPackDialog
725 control={convertToListDialogControl}
726 starterPack={starterPack}
727 />
728 </>
729 )
730}
731
732function InvalidStarterPack({rkey}: {rkey: string}) {
733 const {_} = useLingui()
734 const t = useTheme()
735 const navigation = useNavigation<NavigationProp>()
736 const {gtMobile} = useBreakpoints()
737 const [isProcessing, setIsProcessing] = React.useState(false)
738
739 const goBack = () => {
740 if (navigation.canGoBack()) {
741 navigation.goBack()
742 } else {
743 navigation.replace('Home')
744 }
745 }
746
747 const {mutate: deleteStarterPack} = useDeleteStarterPackMutation({
748 onSuccess: () => {
749 setIsProcessing(false)
750 goBack()
751 },
752 onError: e => {
753 setIsProcessing(false)
754 logger.error('Failed to delete invalid starter pack', {safeMessage: e})
755 Toast.show(_(msg`Failed to delete starter pack`), 'xmark')
756 },
757 })
758
759 return (
760 <Layout.Content centerContent>
761 <View style={[a.py_4xl, a.px_xl, a.align_center, a.gap_5xl]}>
762 <View style={[a.w_full, a.align_center, a.gap_lg]}>
763 <Text style={[a.font_semi_bold, a.text_3xl]}>
764 <Trans>Starter pack is invalid</Trans>
765 </Text>
766 <Text
767 style={[
768 a.text_md,
769 a.text_center,
770 t.atoms.text_contrast_high,
771 {lineHeight: 1.4},
772 gtMobile ? {width: 450} : [a.w_full, a.px_lg],
773 ]}>
774 <Trans>
775 The starter pack that you are trying to view is invalid. You may
776 delete this starter pack instead.
777 </Trans>
778 </Text>
779 </View>
780 <View style={[a.gap_md, gtMobile ? {width: 350} : [a.w_full, a.px_lg]]}>
781 <Button
782 variant="solid"
783 color="primary"
784 label={_(msg`Delete starter pack`)}
785 size="large"
786 style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}
787 disabled={isProcessing}
788 onPress={() => {
789 setIsProcessing(true)
790 deleteStarterPack({rkey})
791 }}>
792 <ButtonText>
793 <Trans>Delete</Trans>
794 </ButtonText>
795 {isProcessing && <Loader size="xs" color="white" />}
796 </Button>
797 <Button
798 variant="solid"
799 color="secondary"
800 label={_(msg`Return to previous page`)}
801 size="large"
802 style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}
803 disabled={isProcessing}
804 onPress={goBack}>
805 <ButtonText>
806 <Trans>Go Back</Trans>
807 </ButtonText>
808 </Button>
809 </View>
810 </View>
811 </Layout.Content>
812 )
813}