about summary refs log tree commit diff
path: root/src/screens/StarterPack/StarterPackLandingScreen.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/screens/StarterPack/StarterPackLandingScreen.tsx')
-rw-r--r--src/screens/StarterPack/StarterPackLandingScreen.tsx378
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>
+  )
+}