Rework web onboarding

zio/stable
Paul Frazee 2023-08-29 22:56:13 -07:00
parent 5d9534ca72
commit 5e765bf1cb
9 changed files with 539 additions and 126 deletions

View File

@ -148,3 +148,110 @@ export const HITSLOP_10 = createHitslop(10)
export const HITSLOP_20 = createHitslop(20) export const HITSLOP_20 = createHitslop(20)
export const HITSLOP_30 = createHitslop(30) export const HITSLOP_30 = createHitslop(30)
export const BACK_HITSLOP = HITSLOP_30 export const BACK_HITSLOP = HITSLOP_30
export const RECOMMENDED_FEEDS = [
{
did: 'did:plc:hsqwcidfez66lwm3gxhfv5in',
rkey: 'aaaf2pqeodmpy',
},
{
did: 'did:plc:gekdk2nd47gkk3utfz2xf7cn',
rkey: 'aaap4tbjcfe5y',
},
{
did: 'did:plc:5rw2on4i56btlcajojaxwcat',
rkey: 'aaao6g552b33o',
},
{
did: 'did:plc:jfhpnnst6flqway4eaeqzj2a',
rkey: 'for-science',
},
{
did: 'did:plc:7q4nnnxawajbfaq7to5dpbsy',
rkey: 'bsky-news',
},
{
did: 'did:plc:jcoy7v3a2t4rcfdh6i4kza25',
rkey: 'astro',
},
{
did: 'did:plc:tenurhgjptubkk5zf5qhi3og',
rkey: 'h-nba',
},
{
did: 'did:plc:vpkhqolt662uhesyj6nxm7ys',
rkey: 'devfeed',
},
{
did: 'did:plc:cndfx4udwgvpjaakvxvh7wm5',
rkey: 'flipboard-tech',
},
{
did: 'did:plc:w4xbfzo7kqfes5zb7r6qv3rw',
rkey: 'blacksky',
},
{
did: 'did:plc:lptjvw6ut224kwrj7ub3sqbe',
rkey: 'aaaotfjzjplna',
},
{
did: 'did:plc:gkvpokm7ec5j5yxls6xk4e3z',
rkey: 'formula-one',
},
{
did: 'did:plc:q6gjnaw2blty4crticxkmujt',
rkey: 'positivifeed',
},
{
did: 'did:plc:l72uci4styb4jucsgcrrj5ap',
rkey: 'aaao5dzfm36u4',
},
{
did: 'did:plc:k3jkadxv5kkjgs6boyon7m6n',
rkey: 'aaaavlyvqzst2',
},
{
did: 'did:plc:nkahctfdi6bxk72umytfwghw',
rkey: 'aaado2uvfsc6w',
},
{
did: 'did:plc:epihigio3d7un7u3gpqiy5gv',
rkey: 'aaaekwsc7zsvs',
},
{
did: 'did:plc:qiknc4t5rq7yngvz7g4aezq7',
rkey: 'aaaejxlobe474',
},
{
did: 'did:plc:mlq4aycufcuolr7ax6sezpc4',
rkey: 'aaaoudweck6uy',
},
{
did: 'did:plc:rcez5hcvq3vzlu5x7xrjyccg',
rkey: 'aaadzjxbcddzi',
},
{
did: 'did:plc:lnxbuzaenlwjrncx6sc4cfdr',
rkey: 'aaab2vesjtszc',
},
{
did: 'did:plc:x3cya3wkt4n6u4ihmvpsc5if',
rkey: 'aaacynbxwimok',
},
{
did: 'did:plc:abv47bjgzjgoh3yrygwoi36x',
rkey: 'aaagt6amuur5e',
},
{
did: 'did:plc:ffkgesg3jsv2j7aagkzrtcvt',
rkey: 'aaacjerk7gwek',
},
{
did: 'did:plc:geoqe3qls5mwezckxxsewys2',
rkey: 'aaai43yetqshu',
},
{
did: 'did:plc:2wqomm3tjqbgktbrfwgvrw34',
rkey: 'authors',
},
]

View File

@ -67,6 +67,19 @@ export class CustomFeedModel {
} }
} }
async pin() {
try {
await this.rootStore.preferences.addPinnedFeed(this.uri)
} catch (error) {
this.rootStore.log.error('Failed to pin feed', error)
} finally {
track('CustomFeed:Pin', {
name: this.data.displayName,
uri: this.uri,
})
}
}
async unsave() { async unsave() {
try { try {
await this.rootStore.preferences.removeSavedFeed(this.uri) await this.rootStore.preferences.removeSavedFeed(this.uri)

View File

@ -6,7 +6,6 @@ import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {CenteredView} from '../util/Views'
import {Welcome} from './onboarding/Welcome' import {Welcome} from './onboarding/Welcome'
import {RecommendedFeeds} from './onboarding/RecommendedFeeds' import {RecommendedFeeds} from './onboarding/RecommendedFeeds'
@ -24,17 +23,15 @@ export const Onboarding = observer(() => {
const skip = () => store.onboarding.skip() const skip = () => store.onboarding.skip()
return ( return (
<CenteredView style={[s.hContentRegion, pal.view]}> <SafeAreaView testID="onboardingView" style={[s.hContentRegion, pal.view]}>
<SafeAreaView testID="noSessionView" style={s.hContentRegion}> <ErrorBoundary>
<ErrorBoundary> {store.onboarding.step === 'Welcome' && (
{store.onboarding.step === 'Welcome' && ( <Welcome skip={skip} next={next} />
<Welcome skip={skip} next={next} /> )}
)} {store.onboarding.step === 'RecommendedFeeds' && (
{store.onboarding.step === 'RecommendedFeeds' && ( <RecommendedFeeds next={next} />
<RecommendedFeeds next={next} /> )}
)} </ErrorBoundary>
</ErrorBoundary> </SafeAreaView>
</SafeAreaView>
</CenteredView>
) )
}) })

View File

@ -9,113 +9,7 @@ import {useCustomFeed} from 'lib/hooks/useCustomFeed'
import {makeRecordUri} from 'lib/strings/url-helpers' import {makeRecordUri} from 'lib/strings/url-helpers'
import {ViewHeader} from 'view/com/util/ViewHeader' import {ViewHeader} from 'view/com/util/ViewHeader'
import {isDesktopWeb} from 'platform/detection' import {isDesktopWeb} from 'platform/detection'
import {RECOMMENDED_FEEDS} from 'lib/constants'
const TEMPORARY_RECOMMENDED_FEEDS = [
{
did: 'did:plc:hsqwcidfez66lwm3gxhfv5in',
rkey: 'aaaf2pqeodmpy',
},
{
did: 'did:plc:gekdk2nd47gkk3utfz2xf7cn',
rkey: 'aaap4tbjcfe5y',
},
{
did: 'did:plc:5rw2on4i56btlcajojaxwcat',
rkey: 'aaao6g552b33o',
},
{
did: 'did:plc:jfhpnnst6flqway4eaeqzj2a',
rkey: 'for-science',
},
{
did: 'did:plc:7q4nnnxawajbfaq7to5dpbsy',
rkey: 'bsky-news',
},
{
did: 'did:plc:jcoy7v3a2t4rcfdh6i4kza25',
rkey: 'astro',
},
{
did: 'did:plc:tenurhgjptubkk5zf5qhi3og',
rkey: 'h-nba',
},
{
did: 'did:plc:vpkhqolt662uhesyj6nxm7ys',
rkey: 'devfeed',
},
{
did: 'did:plc:cndfx4udwgvpjaakvxvh7wm5',
rkey: 'flipboard-tech',
},
{
did: 'did:plc:w4xbfzo7kqfes5zb7r6qv3rw',
rkey: 'blacksky',
},
{
did: 'did:plc:lptjvw6ut224kwrj7ub3sqbe',
rkey: 'aaaotfjzjplna',
},
{
did: 'did:plc:gkvpokm7ec5j5yxls6xk4e3z',
rkey: 'formula-one',
},
{
did: 'did:plc:q6gjnaw2blty4crticxkmujt',
rkey: 'positivifeed',
},
{
did: 'did:plc:l72uci4styb4jucsgcrrj5ap',
rkey: 'aaao5dzfm36u4',
},
{
did: 'did:plc:k3jkadxv5kkjgs6boyon7m6n',
rkey: 'aaaavlyvqzst2',
},
{
did: 'did:plc:nkahctfdi6bxk72umytfwghw',
rkey: 'aaado2uvfsc6w',
},
{
did: 'did:plc:epihigio3d7un7u3gpqiy5gv',
rkey: 'aaaekwsc7zsvs',
},
{
did: 'did:plc:qiknc4t5rq7yngvz7g4aezq7',
rkey: 'aaaejxlobe474',
},
{
did: 'did:plc:mlq4aycufcuolr7ax6sezpc4',
rkey: 'aaaoudweck6uy',
},
{
did: 'did:plc:rcez5hcvq3vzlu5x7xrjyccg',
rkey: 'aaadzjxbcddzi',
},
{
did: 'did:plc:lnxbuzaenlwjrncx6sc4cfdr',
rkey: 'aaab2vesjtszc',
},
{
did: 'did:plc:x3cya3wkt4n6u4ihmvpsc5if',
rkey: 'aaacynbxwimok',
},
{
did: 'did:plc:abv47bjgzjgoh3yrygwoi36x',
rkey: 'aaagt6amuur5e',
},
{
did: 'did:plc:ffkgesg3jsv2j7aagkzrtcvt',
rkey: 'aaacjerk7gwek',
},
{
did: 'did:plc:geoqe3qls5mwezckxxsewys2',
rkey: 'aaai43yetqshu',
},
{
did: 'did:plc:2wqomm3tjqbgktbrfwgvrw34',
rkey: 'authors',
},
]
type Props = { type Props = {
next: () => void next: () => void
@ -132,7 +26,7 @@ export const RecommendedFeeds = observer(({next}: Props) => {
</Text> </Text>
<FlatList <FlatList
data={TEMPORARY_RECOMMENDED_FEEDS} data={RECOMMENDED_FEEDS}
renderItem={({item}) => <Item item={item} />} renderItem={({item}) => <Item item={item} />}
keyExtractor={item => item.did + item.rkey} keyExtractor={item => item.did + item.rkey}
style={{flex: 1}} style={{flex: 1}}

View File

@ -0,0 +1,214 @@
import React from 'react'
import {FlatList, StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Text} from 'view/com/util/text/Text'
import {TitleColumnLayout} from 'view/com/util/layouts/TitleColumnLayout'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {Button} from 'view/com/util/forms/Button'
import * as Toast from 'view/com/util/Toast'
import {usePalette} from 'lib/hooks/usePalette'
import {useCustomFeed} from 'lib/hooks/useCustomFeed'
import {makeRecordUri} from 'lib/strings/url-helpers'
import {sanitizeHandle} from 'lib/strings/handles'
import {HeartIcon} from 'lib/icons'
import {RECOMMENDED_FEEDS} from 'lib/constants'
type Props = {
next: () => void
}
export const RecommendedFeeds = observer(({next}: Props) => {
const pal = usePalette('default')
const title = (
<>
<Text style={[pal.textLight, styles.title1]}>Choose your</Text>
<Text style={[pal.link, styles.title2]}>Recomended</Text>
<Text style={[pal.link, styles.title2]}>Feeds</Text>
<Text type="2xl-medium" style={[pal.textLight, styles.description]}>
Feeds are created by users to curate content. Choose some feeds that you
find interesting.
</Text>
<View
style={{
flexDirection: 'row',
justifyContent: 'flex-end',
marginTop: 20,
}}>
<Button onPress={next} testID="continueBtn">
<View
style={{
flexDirection: 'row',
alignItems: 'center',
paddingLeft: 2,
gap: 6,
}}>
<Text
type="2xl-medium"
style={{color: '#fff', position: 'relative', top: -1}}>
Done
</Text>
<FontAwesomeIcon icon="angle-right" color="#fff" size={14} />
</View>
</Button>
</View>
</>
)
return (
<TitleColumnLayout
testID="recommendedFeedsScreen"
title={title}
horizontal
titleStyle={{minWidth: 470}}
contentStyle={{paddingHorizontal: 0}}>
<FlatList
data={RECOMMENDED_FEEDS}
renderItem={({item}) => <Item {...item} />}
keyExtractor={item => item.did + item.rkey}
style={{flex: 1}}
/>
</TitleColumnLayout>
)
})
const Item = observer(({did, rkey}: {did: string; rkey: string}) => {
const pal = usePalette('default')
const uri = makeRecordUri(did, 'app.bsky.feed.generator', rkey)
const item = useCustomFeed(uri)
if (!item) return null
const onToggle = async () => {
if (item.isSaved) {
try {
await item.unsave()
} catch (e) {
Toast.show('There was an issue contacting your server')
console.error('Failed to unsave feed', {e})
}
} else {
try {
await item.save()
await item.pin()
} catch (e) {
Toast.show('There was an issue contacting your server')
console.error('Failed to pin feed', {e})
}
}
}
return (
<View testID={`feed-${item.displayName}`}>
<View
style={[
pal.border,
{
flexDirection: 'row',
gap: 18,
maxWidth: 670,
borderRightWidth: 1,
paddingHorizontal: 24,
paddingVertical: 24,
borderTopWidth: 1,
},
]}>
<View style={{marginTop: 2}}>
<UserAvatar type="algo" size={42} avatar={item.data.avatar} />
</View>
<View>
<Text
type="2xl-bold"
numberOfLines={1}
style={[pal.text, {fontSize: 19}]}>
{item.displayName}
</Text>
<Text style={[pal.textLight, {marginBottom: 8}]} numberOfLines={1}>
by {sanitizeHandle(item.data.creator.handle, '@')}
</Text>
{item.data.description ? (
<Text
type="xl"
style={[pal.text, {maxWidth: 550, marginBottom: 18}]}
numberOfLines={6}>
{item.data.description}
</Text>
) : null}
<View style={{flexDirection: 'row', alignItems: 'center', gap: 12}}>
<Button
type="inverted"
style={{paddingVertical: 6}}
onPress={onToggle}>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
paddingRight: 2,
gap: 6,
}}>
{item.isSaved ? (
<>
<FontAwesomeIcon
icon="check"
size={16}
color={pal.colors.textInverted}
/>
<Text type="lg-medium" style={pal.textInverted}>
Added
</Text>
</>
) : (
<>
<FontAwesomeIcon
icon="plus"
size={16}
color={pal.colors.textInverted}
/>
<Text type="lg-medium" style={pal.textInverted}>
Add
</Text>
</>
)}
</View>
</Button>
<View style={{flexDirection: 'row', gap: 4}}>
<HeartIcon
size={16}
strokeWidth={2.5}
style={[pal.textLight, {position: 'relative', top: 2}]}
/>
<Text type="lg-medium" style={[pal.text, pal.textLight]}>
{item.data.likeCount || 0}
</Text>
</View>
</View>
</View>
</View>
</View>
)
})
const styles = StyleSheet.create({
container: {
flex: 1,
marginHorizontal: 16,
justifyContent: 'space-between',
},
title1: {
fontSize: 36,
fontWeight: '800',
textAlign: 'right',
},
title2: {
fontSize: 58,
fontWeight: '800',
textAlign: 'right',
},
description: {
maxWidth: 400,
marginTop: 10,
marginLeft: 'auto',
textAlign: 'right',
},
})

View File

@ -41,8 +41,10 @@ export const Welcome = observer(({next, skip}: Props) => {
}} }}
/> />
<View> <View>
<Text style={[pal.text, styles.title]}>Welcome to </Text> <Text style={[pal.text, styles.title]}>
<Text style={[pal.text, pal.link, styles.title]}>Bluesky</Text> Welcome to{' '}
<Text style={[pal.text, pal.link, styles.title]}>Bluesky</Text>
</Text>
<View style={styles.spacer} /> <View style={styles.spacer} />
<View style={[styles.row]}> <View style={[styles.row]}>
<FontAwesomeIcon icon={'globe'} size={36} color={pal.colors.link} /> <FontAwesomeIcon icon={'globe'} size={36} color={pal.colors.link} />
@ -98,7 +100,7 @@ const styles = StyleSheet.create({
justifyContent: 'space-between', justifyContent: 'space-between',
}, },
title: { title: {
fontSize: 48, fontSize: 42,
fontWeight: '800', fontWeight: '800',
}, },
row: { row: {

View File

@ -0,0 +1,123 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {useMediaQuery} from 'react-responsive'
import {Text} from 'view/com/util/text/Text'
import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {TitleColumnLayout} from 'view/com/util/layouts/TitleColumnLayout'
import {Button} from 'view/com/util/forms/Button'
import {observer} from 'mobx-react-lite'
type Props = {
next: () => void
skip: () => void
}
export const Welcome = observer(({next}: Props) => {
const pal = usePalette('default')
const horizontal = useMediaQuery({
query: '(min-width: 1230px)',
})
const title = (
<>
<Text
style={[
pal.textLight,
{
fontSize: 36,
fontWeight: '800',
textAlign: horizontal ? 'right' : 'left',
},
]}>
Welcome to
</Text>
<Text
style={[
pal.link,
{
fontSize: 72,
fontWeight: '800',
textAlign: horizontal ? 'right' : 'left',
},
]}>
Bluesky
</Text>
</>
)
return (
<TitleColumnLayout
testID="welcomeOnboarding"
title={title}
horizontal={horizontal}
titleStyle={horizontal ? {paddingBottom: 160} : undefined}>
<View style={[styles.row]}>
<FontAwesomeIcon icon={'globe'} size={36} color={pal.colors.link} />
<View style={[styles.rowText]}>
<Text type="xl-bold" style={[pal.text]}>
Bluesky is public.
</Text>
<Text type="xl" style={[pal.text, s.pt2]}>
Your posts, likes, and blocks are public. Mutes are private.
</Text>
</View>
</View>
<View style={[styles.row]}>
<FontAwesomeIcon icon={'at'} size={36} color={pal.colors.link} />
<View style={[styles.rowText]}>
<Text type="xl-bold" style={[pal.text]}>
Bluesky is open.
</Text>
<Text type="xl" style={[pal.text, s.pt2]}>
Never lose access to your followers and data.
</Text>
</View>
</View>
<View style={[styles.row]}>
<FontAwesomeIcon icon={'gear'} size={36} color={pal.colors.link} />
<View style={[styles.rowText]}>
<Text type="xl-bold" style={[pal.text]}>
Bluesky is flexible.
</Text>
<Text type="xl" style={[pal.text, s.pt2]}>
Choose the algorithms that power your experience with custom feeds.
</Text>
</View>
</View>
<View style={styles.spacer} />
<View style={{flexDirection: 'row'}}>
<Button onPress={next} testID="continueBtn">
<View
style={{
flexDirection: 'row',
alignItems: 'center',
paddingLeft: 2,
gap: 6,
}}>
<Text
type="2xl-medium"
style={{color: '#fff', position: 'relative', top: -1}}>
Next
</Text>
<FontAwesomeIcon icon="angle-right" color="#fff" size={14} />
</View>
</Button>
</View>
</TitleColumnLayout>
)
})
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
columnGap: 20,
alignItems: 'center',
marginVertical: 20,
},
rowText: {
flex: 1,
},
spacer: {
height: 20,
},
})

View File

@ -0,0 +1,62 @@
import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {usePalette} from 'lib/hooks/usePalette'
interface Props {
testID?: string
title: React.Component
horizontal: boolean
titleStyle?: StyleProp<ViewStyle>
contentStyle?: StyleProp<ViewStyle>
}
export function TitleColumnLayout({
testID,
title,
horizontal,
children,
titleStyle,
contentStyle,
}: React.PropsWithChildren<Props>) {
const pal = usePalette('default')
const layoutStyles = horizontal ? styles2Column : styles1Column
return (
<View testID={testID} style={layoutStyles.container}>
<View style={[layoutStyles.title, pal.viewLight, titleStyle]}>
{title}
</View>
<View style={[layoutStyles.content, contentStyle]}>{children}</View>
</View>
)
}
const styles2Column = StyleSheet.create({
container: {
flexDirection: 'row',
height: '100%',
},
title: {
flex: 1,
paddingHorizontal: 40,
paddingBottom: 80,
justifyContent: 'center',
},
content: {
flex: 2,
paddingHorizontal: 40,
justifyContent: 'center',
},
})
const styles1Column = StyleSheet.create({
container: {},
title: {
paddingHorizontal: 40,
paddingVertical: 40,
},
content: {
paddingHorizontal: 40,
paddingVertical: 40,
},
})

View File

@ -20,7 +20,6 @@ import {NavigationProp} from 'lib/routes/types'
const ShellInner = observer(() => { const ShellInner = observer(() => {
const store = useStores() const store = useStores()
const {isDesktop} = useWebMediaQueries() const {isDesktop} = useWebMediaQueries()
const navigator = useNavigation<NavigationProp>() const navigator = useNavigation<NavigationProp>()
useEffect(() => { useEffect(() => {
@ -29,6 +28,8 @@ const ShellInner = observer(() => {
}) })
}, [navigator, store.shell]) }, [navigator, store.shell])
const showSideNavs =
isDesktop && store.session.hasSession && !store.onboarding.isActive
return ( return (
<> <>
<View style={s.hContentRegion}> <View style={s.hContentRegion}>
@ -36,7 +37,7 @@ const ShellInner = observer(() => {
<FlatNavigator /> <FlatNavigator />
</ErrorBoundary> </ErrorBoundary>
</View> </View>
{isDesktop && store.session.hasSession && ( {showSideNavs && (
<> <>
<DesktopLeftNav /> <DesktopLeftNav />
<DesktopRightNav /> <DesktopRightNav />