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