* Extract ListCard from FeedCard * Export FeedCard.Action and optionally include in ListCard * Remove list dual usage from most of FeedCard * Update usages of FeedCard and ListCard * Add back list purpose logic * Make Action comp easier to use, clarify list purpose * Rename Action to SaveButton
428 lines
13 KiB
TypeScript
428 lines
13 KiB
TypeScript
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 {JOINED_THIS_WEEK} from '#/lib/constants'
|
|
import {isAndroidWeb} from 'lib/browser'
|
|
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 {formatCount} from '#/view/com/util/numeric/format'
|
|
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 {ChevronLeft_Stroke2_Corner0_Rounded} from '#/components/icons/Chevron'
|
|
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,
|
|
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 <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} = 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 = () => {
|
|
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)
|
|
}
|
|
}
|
|
|
|
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,
|
|
},
|
|
]}>
|
|
<Pressable
|
|
style={[
|
|
a.absolute,
|
|
a.rounded_full,
|
|
a.align_center,
|
|
a.justify_center,
|
|
{
|
|
top: 10,
|
|
left: 10,
|
|
height: 35,
|
|
width: 35,
|
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
},
|
|
]}
|
|
onPress={() => {
|
|
setActiveStarterPack(undefined)
|
|
}}
|
|
accessibilityLabel={_(msg`Back`)}
|
|
accessibilityHint={_(msg`Go back to previous screen`)}>
|
|
<ChevronLeft_Stroke2_Corner0_Rounded
|
|
width={20}
|
|
height={20}
|
|
fill="white"
|
|
/>
|
|
</Pressable>
|
|
<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>
|
|
<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}>
|
|
<Trans>{formatCount(JOINED_THIS_WEEK)} joined this week</Trans>
|
|
</Text>
|
|
</View>
|
|
</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
|
|
style={
|
|
isTabletOrDesktop && [
|
|
a.border,
|
|
a.rounded_md,
|
|
t.atoms.border_contrast_low,
|
|
]
|
|
}>
|
|
{starterPack.listItemsSample
|
|
?.filter(p => !p.subject.associated?.labeler)
|
|
.slice(0, 8)
|
|
.map((item, i) => (
|
|
<View
|
|
key={item.subject.did}
|
|
style={[
|
|
a.py_lg,
|
|
a.px_md,
|
|
(!isTabletOrDesktop || i !== 0) && a.border_t,
|
|
t.atoms.border_contrast_low,
|
|
{pointerEvents: 'none'},
|
|
]}>
|
|
<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'},
|
|
isTabletOrDesktop && [
|
|
a.border,
|
|
a.rounded_md,
|
|
t.atoms.border_contrast_low,
|
|
],
|
|
]}>
|
|
{feeds?.map((feed, i) => (
|
|
<View
|
|
style={[
|
|
a.py_lg,
|
|
a.px_md,
|
|
(!isTabletOrDesktop || i !== 0) && a.border_t,
|
|
t.atoms.border_contrast_low,
|
|
]}
|
|
key={feed.uri}>
|
|
<FeedCard.Default 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>
|
|
)
|
|
}
|