New onboarding (#241)

* delete old onboarding files and code

* add custom FollowButton component to Post, FeedItem, & ProfileCard

* move building suggested feed into helper lib

* show suggested posts/feed if follower list is empty

* Update tsconfig.json

* add pagination to getting new onboarding

* remove unnecessary console log

* fix naming, add better null check for combinedCursor

* In locally-combined feeds, correctly produce an undefined cursor when out of data

* Minor refactors of the suggested posts lib functions

* Show 'follow button' style of post meta in certain conditions only

* Only show follow btn in posts on the main feed and the discovery feed

* Add a welcome notice to the home feed

* Tune the timing of when the welcome banner shows or hides

* Make the follow button an observer (closes #244)

* Update postmeta to keep the follow btn after press until next render

* A couple of fixes that ensure consistent welcome screen

* Fix lint

* Rework the welcome banner

* Fix cache invalidation of follows model on user switch

* Show welcome banner while loading

* Update the home onboarding feed to get top posts from hardcode recommends

* Drop unused helper function

* Update happy path tests

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
This commit is contained in:
Ansh 2023-03-02 10:21:33 -08:00 committed by GitHub
parent 9b46b2e6a9
commit bd9386d81c
31 changed files with 426 additions and 866 deletions

View file

@ -33,7 +33,7 @@ export const SuggestedPosts = observer(() => {
<>
<View style={[pal.border, styles.bottomBorder]}>
{suggestedPostsView.posts.map(item => (
<Post item={item} key={item._reactKey} />
<Post item={item} key={item._reactKey} showFollowBtn />
))}
</View>
</>

View file

@ -1,196 +0,0 @@
import React, {useState} from 'react'
import {
Animated,
Image,
SafeAreaView,
StyleSheet,
TouchableOpacity,
useWindowDimensions,
View,
} from 'react-native'
import {TabView, SceneMap, Route, TabBarProps} from 'react-native-tab-view'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {Text} from '../util/text/Text'
import {useStores} from 'state/index'
import {s} from 'lib/styles'
import {TABS_EXPLAINER} from 'lib/assets'
import {TABS_ENABLED} from 'lib/build-flags'
const ROUTES = TABS_ENABLED
? [
{key: 'intro', title: 'Intro'},
{key: 'tabs', title: 'Tabs'},
]
: [{key: 'intro', title: 'Intro'}]
const Intro = () => (
<View style={styles.explainer}>
<Text
style={[styles.explainerHeading, s.normal, styles.explainerHeadingIntro]}>
Welcome to{' '}
<Text style={[s.bold, s.blue3, styles.explainerHeadingBrand]}>
Bluesky
</Text>
</Text>
<Text style={[styles.explainerDesc, styles.explainerDescIntro]}>
This is an early beta. Your feedback is appreciated!
</Text>
</View>
)
const Tabs = () => (
<View style={styles.explainer}>
<View style={styles.explainerIcon}>
<View style={s.flex1} />
<FontAwesomeIcon
icon={['far', 'clone']}
style={[s.black as FontAwesomeIconStyle, s.mb5]}
size={36}
/>
<View style={s.flex1} />
</View>
<Text style={styles.explainerHeading}>Tabs</Text>
<Text style={styles.explainerDesc}>
Never lose your place! Long-press to open posts and profiles in a new tab.
</Text>
<Text style={styles.explainerDesc}>
<Image source={TABS_EXPLAINER} style={styles.explainerImg} />
</Text>
</View>
)
const SCENE_MAP = {
intro: Intro,
tabs: Tabs,
}
const renderScene = SceneMap(SCENE_MAP)
export const FeatureExplainer = () => {
const layout = useWindowDimensions()
const store = useStores()
const [index, setIndex] = useState(0)
const onPressSkip = () => store.onboard.next()
const onPressNext = () => {
if (index >= ROUTES.length - 1) {
store.onboard.next()
} else {
setIndex(index + 1)
}
}
const renderTabBar = (props: TabBarProps<Route>) => {
const inputRange = props.navigationState.routes.map((x, i) => i)
return (
<View style={styles.tabBar}>
<View style={s.flex1} />
{props.navigationState.routes.map((route, i) => {
const opacity = props.position.interpolate({
inputRange,
outputRange: inputRange.map(inputIndex =>
inputIndex === i ? 1 : 0.5,
),
})
return (
<TouchableOpacity
key={i}
style={styles.tabItem}
onPress={() => setIndex(i)}>
<Animated.Text style={{opacity}}>&deg;</Animated.Text>
</TouchableOpacity>
)
})}
<View style={s.flex1} />
</View>
)
}
const FirstExplainer = SCENE_MAP[ROUTES[0]?.key as keyof typeof SCENE_MAP]
return (
<SafeAreaView style={styles.container}>
{ROUTES.length > 1 ? (
<TabView
navigationState={{index, routes: ROUTES}}
renderScene={renderScene}
renderTabBar={renderTabBar}
onIndexChange={setIndex}
initialLayout={{width: layout.width}}
tabBarPosition="bottom"
/>
) : FirstExplainer ? (
<FirstExplainer />
) : (
<View />
)}
<View style={styles.footer}>
<TouchableOpacity
onPress={onPressSkip}
testID="onboardFeatureExplainerSkipBtn">
<Text style={[s.blue3, s.f18]}>Skip</Text>
</TouchableOpacity>
<View style={s.flex1} />
<TouchableOpacity
onPress={onPressNext}
testID="onboardFeatureExplainerNextBtn">
<Text style={[s.blue3, s.f18]}>Next</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
tabBar: {
flexDirection: 'row',
},
tabItem: {
alignItems: 'center',
padding: 16,
},
explainer: {
flex: 1,
paddingHorizontal: 16,
paddingTop: 80,
},
explainerIcon: {
flexDirection: 'row',
},
explainerHeading: {
fontSize: 42,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 16,
},
explainerHeadingIntro: {
lineHeight: 60,
paddingTop: 50,
paddingBottom: 50,
},
explainerHeadingBrand: {fontSize: 56},
explainerDesc: {
fontSize: 18,
textAlign: 'center',
marginBottom: 16,
},
explainerDescIntro: {fontSize: 24},
explainerImg: {
resizeMode: 'contain',
maxWidth: '100%',
maxHeight: 330,
},
footer: {
flexDirection: 'row',
paddingHorizontal: 32,
paddingBottom: 24,
},
})

View file

@ -1,202 +0,0 @@
import React, {useState} from 'react'
import {
Animated,
Image,
StyleSheet,
TouchableOpacity,
useWindowDimensions,
View,
} from 'react-native'
import {TabView, SceneMap, Route, TabBarProps} from 'react-native-tab-view'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {CenteredView} from '../util/Views.web'
import {Text} from '../util/text/Text'
import {useStores} from 'state/index'
import {s, colors} from 'lib/styles'
import {TABS_EXPLAINER} from 'lib/assets'
import {TABS_ENABLED} from 'lib/build-flags'
const ROUTES = TABS_ENABLED
? [
{key: 'intro', title: 'Intro'},
{key: 'tabs', title: 'Tabs'},
]
: [{key: 'intro', title: 'Intro'}]
const Intro = () => (
<View style={styles.explainer}>
<Text
style={[styles.explainerHeading, s.normal, styles.explainerHeadingIntro]}>
Welcome to{' '}
<Text style={[s.bold, s.blue3, styles.explainerHeadingBrand]}>
Bluesky
</Text>
</Text>
<Text style={[styles.explainerDesc, styles.explainerDescIntro]}>
This is an early beta. Your feedback is appreciated!
</Text>
</View>
)
const Tabs = () => (
<View style={styles.explainer}>
<View style={styles.explainerIcon}>
<View style={s.flex1} />
<FontAwesomeIcon
icon={['far', 'clone']}
style={[s.black as FontAwesomeIconStyle, s.mb5]}
size={36}
/>
<View style={s.flex1} />
</View>
<Text style={styles.explainerHeading}>Tabs</Text>
<Text style={styles.explainerDesc}>
Never lose your place! Long-press to open posts and profiles in a new tab.
</Text>
<Text style={styles.explainerDesc}>
<Image source={TABS_EXPLAINER} style={styles.explainerImg} />
</Text>
</View>
)
const SCENE_MAP = {
intro: Intro,
tabs: Tabs,
}
const renderScene = SceneMap(SCENE_MAP)
export const FeatureExplainer = () => {
const layout = useWindowDimensions()
const store = useStores()
const [index, setIndex] = useState(0)
const onPressSkip = () => store.onboard.next()
const onPressNext = () => {
if (index >= ROUTES.length - 1) {
store.onboard.next()
} else {
setIndex(index + 1)
}
}
const renderTabBar = (props: TabBarProps<Route>) => {
const inputRange = props.navigationState.routes.map((x, i) => i)
return (
<View style={styles.tabBar}>
<View style={s.flex1} />
{props.navigationState.routes.map((route, i) => {
const opacity = props.position.interpolate({
inputRange,
outputRange: inputRange.map(inputIndex =>
inputIndex === i ? 1 : 0.5,
),
})
return (
<TouchableOpacity
key={i}
style={styles.tabItem}
onPress={() => setIndex(i)}>
<Animated.Text style={{opacity}}>&deg;</Animated.Text>
</TouchableOpacity>
)
})}
<View style={s.flex1} />
</View>
)
}
const FirstExplainer = SCENE_MAP[ROUTES[0]?.key as keyof typeof SCENE_MAP]
return (
<CenteredView style={styles.container}>
{ROUTES.length > 1 ? (
<TabView
navigationState={{index, routes: ROUTES}}
renderScene={renderScene}
renderTabBar={renderTabBar}
onIndexChange={setIndex}
initialLayout={{width: layout.width}}
tabBarPosition="bottom"
/>
) : FirstExplainer ? (
<FirstExplainer />
) : (
<View />
)}
<View style={styles.footer}>
<TouchableOpacity onPress={onPressSkip}>
<Text style={styles.footerBtn}>Skip</Text>
</TouchableOpacity>
<TouchableOpacity onPress={onPressNext}>
<Text style={[styles.footerBtn, styles.footerBtnNext]}>Next</Text>
</TouchableOpacity>
</View>
</CenteredView>
)
}
const styles = StyleSheet.create({
container: {
height: '100%',
justifyContent: 'center',
paddingBottom: '10%',
},
tabBar: {
flexDirection: 'row',
},
tabItem: {
alignItems: 'center',
padding: 16,
},
explainer: {
paddingHorizontal: 16,
},
explainerIcon: {
flexDirection: 'row',
},
explainerHeading: {
fontSize: 42,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 16,
},
explainerHeadingIntro: {
lineHeight: 40,
},
explainerHeadingBrand: {fontSize: 56},
explainerDesc: {
fontSize: 18,
textAlign: 'center',
marginBottom: 16,
color: colors.gray5,
},
explainerDescIntro: {fontSize: 24},
explainerImg: {
resizeMode: 'contain',
maxWidth: '100%',
maxHeight: 330,
},
footer: {
flexDirection: 'row',
justifyContent: 'center',
paddingTop: 24,
},
footerBtn: {
color: colors.blue3,
fontSize: 19,
paddingHorizontal: 36,
paddingVertical: 8,
},
footerBtnNext: {
marginLeft: 10,
borderWidth: 1,
borderColor: colors.blue3,
borderRadius: 6,
},
})

View file

@ -1,55 +0,0 @@
import React from 'react'
import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native'
import {observer} from 'mobx-react-lite'
import {SuggestedFollows} from '../discover/SuggestedFollows'
import {Text} from '../util/text/Text'
import {useStores} from 'state/index'
import {s} from 'lib/styles'
export const Follows = observer(() => {
const store = useStores()
const onNoSuggestions = () => {
// no suggestions, bounce from this view
store.onboard.next()
}
const onPressNext = () => store.onboard.next()
return (
<SafeAreaView style={styles.container}>
<Text style={styles.title}>Suggested follows</Text>
<View style={s.flex1}>
<SuggestedFollows onNoSuggestions={onNoSuggestions} />
</View>
<View style={styles.footer}>
<TouchableOpacity onPress={onPressNext} testID="onboardFollowsSkipBtn">
<Text style={[s.blue3, s.f18]}>Skip</Text>
</TouchableOpacity>
<View style={s.flex1} />
<TouchableOpacity onPress={onPressNext} testID="onboardFollowsNextBtn">
<Text style={[s.blue3, s.f18]}>Next</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
)
})
const styles = StyleSheet.create({
container: {
flex: 1,
},
title: {
fontSize: 24,
fontWeight: 'bold',
paddingHorizontal: 16,
paddingBottom: 12,
},
footer: {
flexDirection: 'row',
paddingHorizontal: 32,
paddingBottom: 24,
paddingTop: 16,
},
})

View file

@ -1,47 +0,0 @@
import React from 'react'
import {SafeAreaView, StyleSheet, TouchableOpacity} from 'react-native'
import {observer} from 'mobx-react-lite'
import {SuggestedFollows} from '../discover/SuggestedFollows'
import {CenteredView} from '../util/Views.web'
import {Text} from '../util/text/Text'
import {useStores} from 'state/index'
import {s} from 'lib/styles'
export const Follows = observer(() => {
const store = useStores()
const onNoSuggestions = () => {
// no suggestions, bounce from this view
store.onboard.next()
}
const onPressNext = () => store.onboard.next()
return (
<SafeAreaView style={styles.container}>
<CenteredView style={styles.header}>
<Text type="title-lg">
Follow these people to see their posts in your feed
</Text>
<TouchableOpacity onPress={onPressNext}>
<Text style={[styles.title, s.blue3, s.pr10]}>Next &raquo;</Text>
</TouchableOpacity>
</CenteredView>
<SuggestedFollows onNoSuggestions={onNoSuggestions} />
</SafeAreaView>
)
})
const styles = StyleSheet.create({
container: {
flex: 1,
},
title: {
fontSize: 24,
fontWeight: 'bold',
},
header: {
paddingTop: 30,
paddingBottom: 40,
},
})

View file

@ -305,6 +305,8 @@ export const PostThreadItem = observer(function PostThreadItem({
authorHandle={item.post.author.handle}
authorDisplayName={item.post.author.displayName}
timestamp={item.post.indexedAt}
did={item.post.author.did}
declarationCid={item.post.author.declaration.cid}
/>
{item.post.author.viewer?.muted ? (
<View style={[styles.mutedWarning, pal.btn]}>

View file

@ -156,6 +156,8 @@ export const Post = observer(function Post({
authorHandle={item.post.author.handle}
authorDisplayName={item.post.author.displayName}
timestamp={item.post.indexedAt}
did={item.post.author.did}
declarationCid={item.post.author.declaration.cid}
/>
{replyAuthorDid !== '' && (
<View style={[s.flexRow, s.mb2, s.alignCenter]}>

View file

@ -13,16 +13,21 @@ import {EmptyState} from '../util/EmptyState'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {FeedModel} from 'state/models/feed-view'
import {FeedItem} from './FeedItem'
import {WelcomeBanner} from '../util/WelcomeBanner'
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
import {s} from 'lib/styles'
import {useAnalytics} from 'lib/analytics'
import {useStores} from 'state/index'
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
const ERROR_FEED_ITEM = {_reactKey: '__error__'}
const WELCOME_FEED_ITEM = {_reactKey: '__welcome__'}
export const Feed = observer(function Feed({
feed,
style,
showWelcomeBanner,
showPostFollowBtn,
scrollElRef,
onPressTryAgain,
onScroll,
@ -31,6 +36,8 @@ export const Feed = observer(function Feed({
}: {
feed: FeedModel
style?: StyleProp<ViewStyle>
showWelcomeBanner?: boolean
showPostFollowBtn?: boolean
scrollElRef?: MutableRefObject<FlatList<any> | null>
onPressTryAgain?: () => void
onScroll?: OnScrollCb
@ -38,7 +45,9 @@ export const Feed = observer(function Feed({
headerOffset?: number
}) {
const {track} = useAnalytics()
const store = useStores()
const [isRefreshing, setIsRefreshing] = React.useState(false)
const [isNewUser, setIsNewUser] = React.useState<boolean>(false)
const data = React.useMemo(() => {
let feedItems: any[] = []
@ -46,6 +55,9 @@ export const Feed = observer(function Feed({
if (feed.hasError) {
feedItems = feedItems.concat([ERROR_FEED_ITEM])
}
if (showWelcomeBanner && isNewUser) {
feedItems = feedItems.concat([WELCOME_FEED_ITEM])
}
if (feed.isEmpty) {
feedItems = feedItems.concat([EMPTY_FEED_ITEM])
} else {
@ -53,21 +65,39 @@ export const Feed = observer(function Feed({
}
}
return feedItems
}, [feed.hasError, feed.hasLoaded, feed.isEmpty, feed.feed])
}, [
feed.hasError,
feed.hasLoaded,
feed.isEmpty,
feed.feed,
showWelcomeBanner,
isNewUser,
])
// events
// =
const checkWelcome = React.useCallback(async () => {
if (showWelcomeBanner) {
await store.me.follows.fetchIfNeeded()
setIsNewUser(store.me.follows.isEmpty)
}
}, [showWelcomeBanner, store.me.follows])
React.useEffect(() => {
checkWelcome()
}, [checkWelcome])
const onRefresh = React.useCallback(async () => {
track('Feed:onRefresh')
setIsRefreshing(true)
checkWelcome()
try {
await feed.refresh()
} catch (err) {
feed.rootStore.log.error('Failed to refresh posts feed', err)
}
setIsRefreshing(false)
}, [feed, track, setIsRefreshing])
}, [feed, track, setIsRefreshing, checkWelcome])
const onEndReached = React.useCallback(async () => {
track('Feed:onEndReached')
try {
@ -101,10 +131,12 @@ export const Feed = observer(function Feed({
onPressTryAgain={onPressTryAgain}
/>
)
} else if (item === WELCOME_FEED_ITEM) {
return <WelcomeBanner />
}
return <FeedItem item={item} />
return <FeedItem item={item} showFollowBtn={showPostFollowBtn} />
},
[feed, onPressTryAgain],
[feed, onPressTryAgain, showPostFollowBtn],
)
const FeedFooter = React.useCallback(
@ -123,6 +155,7 @@ export const Feed = observer(function Feed({
<View testID={testID} style={style}>
{feed.isLoading && data.length === 0 && (
<CenteredView style={{paddingTop: headerOffset}}>
{showWelcomeBanner && isNewUser && <WelcomeBanner />}
<PostFeedLoadingPlaceholder />
</CenteredView>
)}

View file

@ -26,10 +26,12 @@ import {useAnalytics} from 'lib/analytics'
export const FeedItem = observer(function ({
item,
showReplyLine,
showFollowBtn,
ignoreMuteFor,
}: {
item: FeedItemModel
showReplyLine?: boolean
showFollowBtn?: boolean
ignoreMuteFor?: string
}) {
const store = useStores()
@ -175,6 +177,9 @@ export const FeedItem = observer(function ({
authorHandle={item.post.author.handle}
authorDisplayName={item.post.author.displayName}
timestamp={item.post.indexedAt}
did={item.post.author.did}
declarationCid={item.post.author.declaration.cid}
showFollowBtn={showFollowBtn}
/>
{!isChild && replyAuthorDid !== '' && (
<View style={[s.flexRow, s.mb2, s.alignCenter]}>

View file

@ -0,0 +1,57 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {observer} from 'mobx-react-lite'
import {Text} from '../util/text/Text'
import {useStores} from 'state/index'
import * as apilib from 'lib/api/index'
import * as Toast from '../util/Toast'
import {usePalette} from 'lib/hooks/usePalette'
const FollowButton = observer(
({did, declarationCid}: {did: string; declarationCid: string}) => {
const store = useStores()
const pal = usePalette('default')
const isFollowing = store.me.follows.isFollowing(did)
const onToggleFollow = async () => {
if (store.me.follows.isFollowing(did)) {
try {
await apilib.unfollow(store, store.me.follows.getFollowUri(did))
store.me.follows.removeFollow(did)
} catch (e: any) {
store.log.error('Failed fo delete follow', e)
Toast.show('An issue occurred, please try again.')
}
} else {
try {
const res = await apilib.follow(store, did, declarationCid)
store.me.follows.addFollow(did, res.uri)
} catch (e: any) {
store.log.error('Failed fo create follow', e)
Toast.show('An issue occurred, please try again.')
}
}
}
return (
<TouchableOpacity onPress={onToggleFollow}>
<View style={[styles.btn, pal.btn]}>
<Text type="button" style={[pal.text]}>
{isFollowing ? 'Unfollow' : 'Follow'}
</Text>
</View>
</TouchableOpacity>
)
},
)
export default FollowButton
const styles = StyleSheet.create({
btn: {
paddingVertical: 7,
borderRadius: 50,
marginLeft: 6,
paddingHorizontal: 14,
},
})

View file

@ -1,14 +1,13 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar'
import * as Toast from '../util/Toast'
import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from 'state/index'
import * as apilib from 'lib/api/index'
import FollowButton from './FollowButton'
export function ProfileCard({
handle,
@ -102,26 +101,7 @@ export const ProfileCardWithFollowBtn = observer(
}) => {
const store = useStores()
const isMe = store.me.handle === handle
const isFollowing = store.me.follows.isFollowing(did)
const onToggleFollow = async () => {
if (store.me.follows.isFollowing(did)) {
try {
await apilib.unfollow(store, store.me.follows.getFollowUri(did))
store.me.follows.removeFollow(did)
} catch (e: any) {
store.log.error('Failed fo delete follow', e)
Toast.show('An issue occurred, please try again.')
}
} else {
try {
const res = await apilib.follow(store, did, declarationCid)
store.me.follows.addFollow(did, res.uri)
} catch (e: any) {
store.log.error('Failed fo create follow', e)
Toast.show('An issue occurred, please try again.')
}
}
}
return (
<ProfileCard
handle={handle}
@ -132,34 +112,13 @@ export const ProfileCardWithFollowBtn = observer(
renderButton={
isMe
? undefined
: () => (
<FollowBtn isFollowing={isFollowing} onPress={onToggleFollow} />
)
: () => <FollowButton did={did} declarationCid={declarationCid} />
}
/>
)
},
)
function FollowBtn({
isFollowing,
onPress,
}: {
isFollowing: boolean
onPress: () => void
}) {
const pal = usePalette('default')
return (
<TouchableOpacity onPress={onPress}>
<View style={[styles.btn, pal.btn]}>
<Text type="button" style={[pal.text]}>
{isFollowing ? 'Unfollow' : 'Follow'}
</Text>
</View>
</TouchableOpacity>
)
}
const styles = StyleSheet.create({
outer: {
borderTopWidth: 1,

View file

@ -1,37 +1,74 @@
import React from 'react'
import {Platform, StyleSheet, View} from 'react-native'
import {StyleSheet, View} from 'react-native'
import {Text} from './text/Text'
import {ago} from 'lib/strings/time'
import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from 'state/index'
import {observer} from 'mobx-react-lite'
import FollowButton from '../profile/FollowButton'
interface PostMetaOpts {
authorHandle: string
authorDisplayName: string | undefined
timestamp: string
did: string
declarationCid: string
showFollowBtn?: boolean
}
export function PostMeta(opts: PostMetaOpts) {
export const PostMeta = observer(function (opts: PostMetaOpts) {
const pal = usePalette('default')
let displayName = opts.authorDisplayName || opts.authorHandle
let handle = opts.authorHandle
const store = useStores()
const isMe = opts.did === store.me.did
// HACK
// Android simply cannot handle the truncation case we need
// so we have to do it manually here
// -prf
if (Platform.OS === 'android') {
if (displayName.length + handle.length > 26) {
if (displayName.length > 26) {
displayName = displayName.slice(0, 23) + '...'
} else {
handle = handle.slice(0, 23 - displayName.length) + '...'
if (handle.endsWith('....')) {
handle = handle.slice(0, -4) + '...'
}
}
}
// NOTE we capture `isFollowing` via a memo so that follows
// don't change this UI immediately, but rather upon future
// renders
const isFollowing = React.useMemo(
() => store.me.follows.isFollowing(opts.did),
[opts.did, store.me.follows],
)
if (opts.showFollowBtn && !isMe && !isFollowing) {
// two-liner with follow button
return (
<View style={[styles.metaTwoLine]}>
<View>
<Text
type="lg-bold"
style={[pal.text]}
numberOfLines={1}
lineHeight={1.2}>
{displayName}{' '}
<Text
type="md"
style={[styles.metaItem, pal.textLight]}
lineHeight={1.2}>
&middot; {ago(opts.timestamp)}
</Text>
</Text>
<Text
type="md"
style={[styles.metaItem, pal.textLight]}
lineHeight={1.2}>
{handle ? (
<Text type="md" style={[pal.textLight]}>
@{handle}
</Text>
) : undefined}
</Text>
</View>
<View>
<FollowButton did={opts.did} declarationCid={opts.declarationCid} />
</View>
</View>
)
}
// one-liner
return (
<View style={styles.meta}>
<View style={[styles.metaItem, styles.maxWidth]}>
@ -53,13 +90,18 @@ export function PostMeta(opts: PostMetaOpts) {
</Text>
</View>
)
}
})
const styles = StyleSheet.create({
meta: {
flexDirection: 'row',
alignItems: 'baseline',
paddingTop: 0,
paddingBottom: 2,
},
metaTwoLine: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingBottom: 2,
},
metaItem: {

View file

@ -0,0 +1,33 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {usePalette} from 'lib/hooks/usePalette'
import {Text} from './text/Text'
import {s} from 'lib/styles'
export function WelcomeBanner() {
const pal = usePalette('default')
return (
<View
testID="welcomeBanner"
style={[pal.view, styles.container, pal.border]}>
<Text
type="title-lg"
style={[pal.text, s.textCenter, s.bold, s.pb5]}
lineHeight={1.1}>
Welcome to the private beta!
</Text>
<Text type="lg" style={[pal.text, s.textCenter]}>
Here are some recent posts. Follow their creators to build your feed.
</Text>
</View>
)
}
const styles = StyleSheet.create({
container: {
paddingTop: 30,
paddingBottom: 26,
paddingHorizontal: 20,
borderTopWidth: 1,
},
})

View file

@ -71,8 +71,6 @@ export const Home = observer(function Home({navIdx, visible}: ScreenParams) {
store.log.debug('HomeScreen: Updating feed')
if (store.me.mainFeed.hasContent) {
store.me.mainFeed.update()
} else {
store.me.mainFeed.setup()
}
return cleanup
}, [visible, store, store.me.mainFeed, navIdx, doPoll, wasVisible, scrollToTop, screen])
@ -97,6 +95,8 @@ export const Home = observer(function Home({navIdx, visible}: ScreenParams) {
feed={store.me.mainFeed}
scrollElRef={scrollElRef}
style={s.hContentRegion}
showWelcomeBanner
showPostFollowBtn
onPressTryAgain={onPressTryAgain}
onScroll={onMainScroll}
headerOffset={HEADER_HEIGHT}

View file

@ -1,40 +0,0 @@
import React, {useEffect} from 'react'
import {StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite'
import {FeatureExplainer} from '../com/onboard/FeatureExplainer'
import {Follows} from '../com/onboard/Follows'
import {OnboardStage, OnboardStageOrder} from 'state/models/onboard'
import {useStores} from 'state/index'
export const Onboard = observer(() => {
const store = useStores()
useEffect(() => {
// sanity check - bounce out of onboarding if the stage is wrong somehow
if (!OnboardStageOrder.includes(store.onboard.stage)) {
store.onboard.stop()
}
}, [store.onboard])
let Com
if (store.onboard.stage === OnboardStage.Explainers) {
Com = FeatureExplainer
} else if (store.onboard.stage === OnboardStage.Follows) {
Com = Follows
} else {
Com = View
}
return (
<View style={styles.container}>
<Com />
</View>
)
})
const styles = StyleSheet.create({
container: {
height: '100%',
backgroundColor: '#fff',
},
})

View file

@ -26,7 +26,6 @@ import {
import {match, MatchResult} from '../../routes'
import {Login} from '../../screens/Login'
import {Menu} from './Menu'
import {Onboard} from '../../screens/Onboard'
import {HorzSwipe} from '../../com/util/gestures/HorzSwipe'
import {ModalsContainer} from '../../com/modals/Modal'
import {Lightbox} from '../../com/lightbox/Lightbox'
@ -408,17 +407,6 @@ export const MobileShell: React.FC = observer(() => {
</View>
)
}
if (store.onboard.isOnboarding) {
return (
<View testID="onboardOuterView" style={styles.outerContainer}>
<View style={styles.innerContainer}>
<ErrorBoundary>
<Onboard />
</ErrorBoundary>
</View>
</View>
)
}
const isAtHome =
store.nav.tab.current.url === TabPurposeMainPath[TabPurpose.Default]

View file

@ -6,7 +6,6 @@ import {useStores} from 'state/index'
import {NavigationModel} from 'state/models/navigation'
import {match, MatchResult} from '../../routes'
import {DesktopHeader} from './DesktopHeader'
import {Onboard} from '../../screens/Onboard'
import {Login} from '../../screens/Login'
import {ErrorBoundary} from '../../com/util/ErrorBoundary'
import {Lightbox} from '../../com/lightbox/Lightbox'
@ -35,15 +34,6 @@ export const WebShell: React.FC = observer(() => {
</View>
)
}
if (store.onboard.isOnboarding) {
return (
<View style={styles.outerContainer}>
<ErrorBoundary>
<Onboard />
</ErrorBoundary>
</View>
)
}
return (
<View style={[styles.outerContainer, pageBg]}>