import React from 'react' import {Pressable, View} from 'react-native' import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' import { AppBskyGraphDefs, AppBskyGraphStarterpack, AtUri, type ModerationOpts, } from '@atproto/api' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {isAndroidWeb} from '#/lib/browser' import {JOINED_THIS_WEEK} from '#/lib/constants' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {logEvent} from '#/lib/statsig/statsig' import {createStarterPackGooglePlayUri} from '#/lib/strings/starter-pack' import {isWeb} from '#/platform/detection' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useStarterPackQuery} from '#/state/queries/starter-packs' import { useActiveStarterPack, useSetActiveStarterPack, } from '#/state/shell/starter-pack' import {LoggedOutScreenState} from '#/view/com/auth/LoggedOut' import {formatCount} from '#/view/com/util/numeric/format' import {Logo} from '#/view/icons/Logo' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import {useDialogControl} from '#/components/Dialog' import * as FeedCard from '#/components/FeedCard' import {useRichText} from '#/components/hooks/useRichText' import * as Layout from '#/components/Layout' import {LinearGradientBackground} from '#/components/LinearGradientBackground' import {ListMaybePlaceholder} from '#/components/Lists' import {Default as ProfileCard} from '#/components/ProfileCard' import * as Prompt from '#/components/Prompt' import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' import * as bsky from '#/types/bsky' const AnimatedPressable = Animated.createAnimatedComponent(Pressable) interface AppClipMessage { action: 'present' | 'store' keyToStoreAs?: string jsonToStore?: string } export function postAppClipMessage(message: AppClipMessage) { // @ts-expect-error safari webview only window.webkit.messageHandlers.onMessage.postMessage(JSON.stringify(message)) } export function LandingScreen({ setScreenState, }: { setScreenState: (state: LoggedOutScreenState) => void }) { const moderationOpts = useModerationOpts() const activeStarterPack = useActiveStarterPack() const { data: starterPack, isError: isErrorStarterPack, isFetching, } = useStarterPackQuery({ uri: activeStarterPack?.uri, }) const isValid = starterPack && starterPack.list && AppBskyGraphDefs.validateStarterPackView(starterPack) && AppBskyGraphStarterpack.validateRecord(starterPack.record) React.useEffect(() => { if (isErrorStarterPack || (starterPack && !isValid)) { setScreenState(LoggedOutScreenState.S_LoginOrCreateAccount) } }, [isErrorStarterPack, setScreenState, isValid, starterPack]) if (isFetching || !starterPack || !isValid || !moderationOpts) { return } // Just for types, this cannot be hit if ( !bsky.dangerousIsType( starterPack.record, AppBskyGraphStarterpack.isRecord, ) ) { return null } return ( ) } function LandingScreenLoaded({ starterPack, starterPackRecord: record, setScreenState, // TODO apply this to profile card moderationOpts, }: { starterPack: AppBskyGraphDefs.StarterPackView starterPackRecord: AppBskyGraphStarterpack.Record setScreenState: (state: LoggedOutScreenState) => void moderationOpts: ModerationOpts }) { const {creator, listItemsSample, feeds} = starterPack const {_, i18n} = useLingui() const t = useTheme() const activeStarterPack = useActiveStarterPack() const setActiveStarterPack = useSetActiveStarterPack() const {isTabletOrDesktop} = useWebMediaQueries() const androidDialogControl = useDialogControl() const [descriptionRt] = useRichText(record.description || '') const [appClipOverlayVisible, setAppClipOverlayVisible] = React.useState(false) const listItemsCount = starterPack.list?.listItemCount ?? 0 const onContinue = () => { setScreenState(LoggedOutScreenState.S_CreateAccount) } const onJoinPress = () => { if (activeStarterPack?.isClip) { setAppClipOverlayVisible(true) postAppClipMessage({ action: 'present', }) } else if (isAndroidWeb) { androidDialogControl.open() } else { onContinue() } logEvent('starterPack:ctaPress', { starterPack: starterPack.uri, }) } const onJoinWithoutPress = () => { if (activeStarterPack?.isClip) { setAppClipOverlayVisible(true) postAppClipMessage({ action: 'present', }) } else { setActiveStarterPack(undefined) setScreenState(LoggedOutScreenState.S_CreateAccount) } } return ( {record.name} Starter pack by {`@${creator.handle}`} {record.description ? ( ) : null} {formatCount(i18n, JOINED_THIS_WEEK)} joined this week {Boolean(listItemsSample?.length) && ( {listItemsCount <= 8 ? ( You'll follow these people right away ) : ( You'll follow these people and {listItemsCount - 8} others )} {starterPack.listItemsSample ?.filter(p => !p.subject.associated?.labeler) .slice(0, 8) .map((item, i) => ( ))} )} {feeds?.length ? ( You'll stay updated with these feeds {feeds?.map((feed, i) => ( ))} ) : null} Download Bluesky The experience is better in the app. Download Bluesky now and we'll pick back up where you left off. { const rkey = new AtUri(starterPack.uri).rkey if (!rkey) return const googlePlayUri = createStarterPackGooglePlayUri( creator.handle, rkey, ) if (!googlePlayUri) return window.location.href = googlePlayUri }} /> {isWeb && ( )} ) } export function AppClipOverlay({ visible, setIsVisible, }: { visible: boolean setIsVisible: (visible: boolean) => void }) { if (!visible) return return ( setIsVisible(false)}> {/* Webkit needs this to have a zindex of 2? */} Download Bluesky to get started! We'll remember the starter pack you chose and use it when you create an account in the app. ) }