diff options
Diffstat (limited to 'src/screens/StarterPack/StarterPackLandingScreen.tsx')
-rw-r--r-- | src/screens/StarterPack/StarterPackLandingScreen.tsx | 378 |
1 files changed, 378 insertions, 0 deletions
diff --git a/src/screens/StarterPack/StarterPackLandingScreen.tsx b/src/screens/StarterPack/StarterPackLandingScreen.tsx new file mode 100644 index 000000000..1c9587a79 --- /dev/null +++ b/src/screens/StarterPack/StarterPackLandingScreen.tsx @@ -0,0 +1,378 @@ +import React from 'react' +import {Pressable, ScrollView, View} from 'react-native' +import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' +import { + AppBskyGraphDefs, + AppBskyGraphStarterpack, + AtUri, + 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 {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +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 {CenteredView} from 'view/com/util/Views' +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 {LinearGradientBackground} from '#/components/LinearGradientBackground' +import {ListMaybePlaceholder} from '#/components/Lists' +import {Default as ProfileCard} from '#/components/ProfileCard' +import * as Prompt from '#/components/Prompt' +import {Text} from '#/components/Typography' + +const AnimatedPressable = Animated.createAnimatedComponent(Pressable) + +interface AppClipMessage { + action: 'present' | 'store' + keyToStoreAs?: string + jsonToStore?: string +} + +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} = 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 (!starterPack || !isValid || !moderationOpts) { + return <ListMaybePlaceholder isLoading={true} /> + } + + return ( + <LandingScreenLoaded + starterPack={starterPack} + setScreenState={setScreenState} + moderationOpts={moderationOpts} + /> + ) +} + +function LandingScreenLoaded({ + starterPack, + setScreenState, + // TODO apply this to profile card + + moderationOpts, +}: { + starterPack: AppBskyGraphDefs.StarterPackView + setScreenState: (state: LoggedOutScreenState) => void + moderationOpts: ModerationOpts +}) { + const {record, creator, listItemsSample, feeds, joinedWeekCount} = starterPack + const {_} = useLingui() + const t = useTheme() + const activeStarterPack = useActiveStarterPack() + const setActiveStarterPack = useSetActiveStarterPack() + const {isTabletOrDesktop} = useWebMediaQueries() + const androidDialogControl = useDialogControl() + + const [appClipOverlayVisible, setAppClipOverlayVisible] = + React.useState(false) + + const listItemsCount = starterPack.list?.listItemCount ?? 0 + + const onContinue = () => { + setActiveStarterPack({ + uri: starterPack.uri, + }) + setScreenState(LoggedOutScreenState.S_CreateAccount) + } + + const onJoinPress = () => { + if (activeStarterPack?.isClip) { + setAppClipOverlayVisible(true) + postAppClipMessage({ + action: 'present', + }) + } else if (isAndroidWeb) { + androidDialogControl.open() + } else { + onContinue() + } + } + + const onJoinWithoutPress = () => { + if (activeStarterPack?.isClip) { + setAppClipOverlayVisible(true) + postAppClipMessage({ + action: 'present', + }) + } else { + setActiveStarterPack(undefined) + setScreenState(LoggedOutScreenState.S_CreateAccount) + } + } + + if (!AppBskyGraphStarterpack.isRecord(record)) { + return null + } + + return ( + <CenteredView style={a.flex_1}> + <ScrollView + style={[a.flex_1, t.atoms.bg]} + contentContainerStyle={{paddingBottom: 100}}> + <LinearGradientBackground + style={[ + a.align_center, + a.gap_sm, + a.px_lg, + a.py_2xl, + isTabletOrDesktop && [a.mt_2xl, a.rounded_md], + activeStarterPack?.isClip && { + paddingTop: 100, + }, + ]}> + <View style={[a.flex_row, a.gap_md, a.pb_sm]}> + <Logo width={76} fill="white" /> + </View> + <Text + style={[ + a.font_bold, + a.text_4xl, + a.text_center, + a.leading_tight, + {color: 'white'}, + ]}> + {record.name} + </Text> + <Text + style={[ + a.text_center, + a.font_semibold, + a.text_md, + {color: 'white'}, + ]}> + Starter pack by {`@${creator.handle}`} + </Text> + </LinearGradientBackground> + <View style={[a.gap_2xl, a.mx_lg, a.my_2xl]}> + {record.description ? ( + <Text style={[a.text_md, t.atoms.text_contrast_medium]}> + {record.description} + </Text> + ) : null} + <View style={[a.gap_sm]}> + <Button + label={_(msg`Join Bluesky`)} + onPress={onJoinPress} + variant="solid" + color="primary" + size="large"> + <ButtonText style={[a.text_lg]}> + <Trans>Join Bluesky</Trans> + </ButtonText> + </Button> + {joinedWeekCount && joinedWeekCount >= 25 ? ( + <View style={[a.flex_row, a.align_center, a.gap_sm]}> + <FontAwesomeIcon + icon="arrow-trend-up" + size={12} + color={t.atoms.text_contrast_medium.color} + /> + <Text + style={[ + a.font_semibold, + a.text_sm, + t.atoms.text_contrast_medium, + ]} + numberOfLines={1}> + 123,659 joined this week + </Text> + </View> + ) : null} + </View> + <View style={[a.gap_3xl]}> + {Boolean(listItemsSample?.length) && ( + <View style={[a.gap_md]}> + <Text style={[a.font_heavy, a.text_lg]}> + {listItemsCount <= 8 ? ( + <Trans>You'll follow these people right away</Trans> + ) : ( + <Trans> + You'll follow these people and {listItemsCount - 8} others + </Trans> + )} + </Text> + <View> + {starterPack.listItemsSample?.slice(0, 8).map(item => ( + <View + key={item.subject.did} + style={[ + a.py_lg, + a.px_md, + a.border_t, + t.atoms.border_contrast_low, + ]}> + <ProfileCard + profile={item.subject} + moderationOpts={moderationOpts} + /> + </View> + ))} + </View> + </View> + )} + {feeds?.length ? ( + <View style={[a.gap_md]}> + <Text style={[a.font_heavy, a.text_lg]}> + <Trans>You'll stay updated with these feeds</Trans> + </Text> + + <View style={[{pointerEvents: 'none'}]}> + {feeds?.map(feed => ( + <View + style={[ + a.py_lg, + a.px_md, + a.border_t, + t.atoms.border_contrast_low, + ]} + key={feed.uri}> + <FeedCard.Default type="feed" view={feed} /> + </View> + ))} + </View> + </View> + ) : null} + </View> + <Button + label={_(msg`Signup without a starter pack`)} + variant="solid" + color="secondary" + size="medium" + style={[a.py_lg]} + onPress={onJoinWithoutPress}> + <ButtonText> + <Trans>Signup without a starter pack</Trans> + </ButtonText> + </Button> + </View> + </ScrollView> + <AppClipOverlay + visible={appClipOverlayVisible} + setIsVisible={setAppClipOverlayVisible} + /> + <Prompt.Outer control={androidDialogControl}> + <Prompt.TitleText> + <Trans>Download Bluesky</Trans> + </Prompt.TitleText> + <Prompt.DescriptionText> + <Trans> + The experience is better in the app. Download Bluesky now and we'll + pick back up where you left off. + </Trans> + </Prompt.DescriptionText> + <Prompt.Actions> + <Prompt.Action + cta="Download on Google Play" + color="primary" + onPress={() => { + const rkey = new AtUri(starterPack.uri).rkey + if (!rkey) return + + const googlePlayUri = createStarterPackGooglePlayUri( + creator.handle, + rkey, + ) + if (!googlePlayUri) return + + window.location.href = googlePlayUri + }} + /> + <Prompt.Action + cta="Continue on web" + color="secondary" + onPress={onContinue} + /> + </Prompt.Actions> + </Prompt.Outer> + {isWeb && ( + <meta + name="apple-itunes-app" + content="app-id=xyz.blueskyweb.app, app-clip-bundle-id=xyz.blueskyweb.app.AppClip, app-clip-display=card" + /> + )} + </CenteredView> + ) +} + +function AppClipOverlay({ + visible, + setIsVisible, +}: { + visible: boolean + setIsVisible: (visible: boolean) => void +}) { + if (!visible) return + + return ( + <AnimatedPressable + accessibilityRole="button" + style={[ + a.absolute, + { + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0, 0, 0, 0.95)', + zIndex: 1, + }, + ]} + entering={FadeIn} + exiting={FadeOut} + onPress={() => setIsVisible(false)}> + <View style={[a.flex_1, a.px_lg, {marginTop: 250}]}> + {/* Webkit needs this to have a zindex of 2? */} + <View style={[a.gap_md, {zIndex: 2}]}> + <Text + style={[a.font_bold, a.text_4xl, {lineHeight: 40, color: 'white'}]}> + Download Bluesky to get started! + </Text> + <Text style={[a.text_lg, {color: 'white'}]}> + We'll remember the starter pack you chose and use it when you create + an account in the app. + </Text> + </View> + </View> + </AnimatedPressable> + ) +} |