Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at readme-update 790 lines 25 kB view raw
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}