Bluesky app fork with some witchin' additions 馃挮
at main 813 lines 26 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} 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}