Onboarding tweaks (#272)

* Small fix to side menu rendering

* Change onboarding to use an explicit 'is onboarding' mode to more clearly control the flow

* Add a progress bar to the welcome banner

* Dont show the 'unfollow button' on posts in weird times (close #271)

* Improve the empty state of the feed

* Only suggest recent posts
zio/stable
Paul Frazee 2023-03-06 15:34:22 -06:00 committed by GitHub
parent 74c30c60b8
commit 36791e68b3
13 changed files with 259 additions and 123 deletions

View File

@ -37,13 +37,20 @@ function mergePosts(
// filter the feed down to the post with the most upvotes // filter the feed down to the post with the most upvotes
res.data.feed = res.data.feed.reduce( res.data.feed = res.data.feed.reduce(
(acc: AppBskyFeedFeedViewPost.Main[], v) => { (acc: AppBskyFeedFeedViewPost.Main[], v) => {
if (!acc?.[0] && !v.reason) { if (
!acc?.[0] &&
!v.reason &&
!v.reply &&
isRecentEnough(v.post.indexedAt)
) {
return [v] return [v]
} }
if ( if (
acc && acc &&
!v.reason && !v.reason &&
v.post.upvoteCount > acc[0].post.upvoteCount !v.reply &&
v.post.upvoteCount > acc[0]?.post.upvoteCount &&
isRecentEnough(v.post.indexedAt)
) { ) {
return [v] return [v]
} }
@ -112,6 +119,16 @@ function isCombinedCursor(cursor: string) {
return cursor.includes(',') return cursor.includes(',')
} }
const TWO_DAYS_AGO = Date.now() - 1e3 * 60 * 60 * 48
function isRecentEnough(date: string) {
try {
const d = Number(new Date(date))
return d > TWO_DAYS_AGO
} catch {
return false
}
}
export { export {
getMultipleAuthorsPosts, getMultipleAuthorsPosts,
mergePosts, mergePosts,

View File

@ -212,7 +212,7 @@ export class FeedModel {
constructor( constructor(
public rootStore: RootStoreModel, public rootStore: RootStoreModel,
public feedType: 'home' | 'author', public feedType: 'home' | 'author' | 'suggested',
params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams, params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams,
) { ) {
makeAutoObservable( makeAutoObservable(
@ -256,7 +256,7 @@ export class FeedModel {
item.reply?.root.author.did === item.post.author.did) item.reply?.root.author.did === item.post.author.did)
) )
}) })
} else { } else if (this.feedType === 'home') {
return this.feed.filter(item => { return this.feed.filter(item => {
const isRepost = Boolean(item?.reasonRepost) const isRepost = Boolean(item?.reasonRepost)
return ( return (
@ -267,6 +267,8 @@ export class FeedModel {
item.post.upvoteCount >= 2 item.post.upvoteCount >= 2
) )
}) })
} else {
return this.feed
} }
} }
@ -293,6 +295,14 @@ export class FeedModel {
this.feed = [] this.feed = []
} }
switchFeedType(feedType: 'home' | 'suggested') {
if (this.feedType === feedType) {
return
}
this.feedType = feedType
return this.setup()
}
/** /**
* Load for first render * Load for first render
*/ */
@ -427,7 +437,7 @@ export class FeedModel {
* Check if new posts are available * Check if new posts are available
*/ */
async checkForLatest() { async checkForLatest() {
if (this.hasNewLatest || this.rootStore.me.follows.isEmpty) { if (this.hasNewLatest || this.feedType === 'suggested') {
return return
} }
const res = await this._getFeed({limit: 1}) const res = await this._getFeed({limit: 1})
@ -562,15 +572,10 @@ export class FeedModel {
params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams = {}, params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams = {},
): Promise<GetTimeline.Response | GetAuthorFeed.Response> { ): Promise<GetTimeline.Response | GetAuthorFeed.Response> {
params = Object.assign({}, this.params, params) params = Object.assign({}, this.params, params)
if (this.feedType === 'home') { if (this.feedType === 'suggested') {
await this.rootStore.me.follows.fetchIfNeeded()
if (this.rootStore.me.follows.isEmpty) {
const responses = await getMultipleAuthorsPosts( const responses = await getMultipleAuthorsPosts(
this.rootStore, this.rootStore,
sampleSize( sampleSize(SUGGESTED_FOLLOWS(String(this.rootStore.agent.service)), 20),
SUGGESTED_FOLLOWS(String(this.rootStore.agent.service)),
20,
),
params.before, params.before,
20, 20,
) )
@ -585,7 +590,7 @@ export class FeedModel {
}, },
headers: lastHeaders, headers: lastHeaders,
} }
} } else if (this.feedType === 'home') {
return this.rootStore.api.app.bsky.feed.getTimeline( return this.rootStore.api.app.bsky.feed.getTimeline(
params as GetTimeline.QueryParams, params as GetTimeline.QueryParams,
) )

View File

@ -72,6 +72,10 @@ export class MyFollowsModel {
return !!this.followDidToRecordMap[did] return !!this.followDidToRecordMap[did]
} }
get numFollows() {
return Object.keys(this.followDidToRecordMap).length
}
get isEmpty() { get isEmpty() {
return Object.keys(this.followDidToRecordMap).length === 0 return Object.keys(this.followDidToRecordMap).length === 0
} }

View File

@ -345,6 +345,7 @@ export class SessionModel {
) )
this.setActiveSession(agent, did) this.setActiveSession(agent, did)
this.rootStore.shell.setOnboarding(true)
this.rootStore.log.debug('SessionModel:createAccount succeeded') this.rootStore.log.debug('SessionModel:createAccount succeeded')
} }

View File

@ -118,6 +118,7 @@ export class ShellUiModel {
activeLightbox: ProfileImageLightbox | ImagesLightbox | undefined activeLightbox: ProfileImageLightbox | ImagesLightbox | undefined
isComposerActive = false isComposerActive = false
composerOpts: ComposerOpts | undefined composerOpts: ComposerOpts | undefined
isOnboarding = false
constructor(public rootStore: RootStoreModel) { constructor(public rootStore: RootStoreModel) {
makeAutoObservable(this, { makeAutoObservable(this, {
@ -185,4 +186,13 @@ export class ShellUiModel {
this.isComposerActive = false this.isComposerActive = false
this.composerOpts = undefined this.composerOpts = undefined
} }
setOnboarding(v: boolean) {
this.isOnboarding = v
if (this.isOnboarding) {
this.rootStore.me.mainFeed.switchFeedType('suggested')
} else {
this.rootStore.me.mainFeed.switchFeedType('home')
}
}
} }

View File

@ -7,26 +7,28 @@ import {
StyleSheet, StyleSheet,
ViewStyle, ViewStyle,
} from 'react-native' } from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
import {CenteredView, FlatList} from '../util/Views' import {CenteredView, FlatList} from '../util/Views'
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {EmptyState} from '../util/EmptyState' import {Text} from '../util/text/Text'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {Button} from '../util/forms/Button'
import {FeedModel} from 'state/models/feed-view' import {FeedModel} from 'state/models/feed-view'
import {FeedItem} from './FeedItem' import {FeedItem} from './FeedItem'
import {WelcomeBanner} from '../util/WelcomeBanner'
import {OnScrollCb} from 'lib/hooks/useOnMainScroll' import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {useAnalytics} from 'lib/analytics'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {useAnalytics} from 'lib/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {MagnifyingGlassIcon} from 'lib/icons'
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
const ERROR_FEED_ITEM = {_reactKey: '__error__'} const ERROR_FEED_ITEM = {_reactKey: '__error__'}
const WELCOME_FEED_ITEM = {_reactKey: '__welcome__'}
export const Feed = observer(function Feed({ export const Feed = observer(function Feed({
feed, feed,
style, style,
showWelcomeBanner,
showPostFollowBtn, showPostFollowBtn,
scrollElRef, scrollElRef,
onPressTryAgain, onPressTryAgain,
@ -36,7 +38,6 @@ export const Feed = observer(function Feed({
}: { }: {
feed: FeedModel feed: FeedModel
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
showWelcomeBanner?: boolean
showPostFollowBtn?: boolean showPostFollowBtn?: boolean
scrollElRef?: MutableRefObject<FlatList<any> | null> scrollElRef?: MutableRefObject<FlatList<any> | null>
onPressTryAgain?: () => void onPressTryAgain?: () => void
@ -44,10 +45,11 @@ export const Feed = observer(function Feed({
testID?: string testID?: string
headerOffset?: number headerOffset?: number
}) { }) {
const {track} = useAnalytics() const pal = usePalette('default')
const palInverted = usePalette('inverted')
const store = useStores() const store = useStores()
const {track} = useAnalytics()
const [isRefreshing, setIsRefreshing] = React.useState(false) const [isRefreshing, setIsRefreshing] = React.useState(false)
const [isNewUser, setIsNewUser] = React.useState<boolean>(false)
const data = React.useMemo(() => { const data = React.useMemo(() => {
let feedItems: any[] = [] let feedItems: any[] = []
@ -55,9 +57,6 @@ export const Feed = observer(function Feed({
if (feed.hasError) { if (feed.hasError) {
feedItems = feedItems.concat([ERROR_FEED_ITEM]) feedItems = feedItems.concat([ERROR_FEED_ITEM])
} }
if (showWelcomeBanner && isNewUser) {
feedItems = feedItems.concat([WELCOME_FEED_ITEM])
}
if (feed.isEmpty) { if (feed.isEmpty) {
feedItems = feedItems.concat([EMPTY_FEED_ITEM]) feedItems = feedItems.concat([EMPTY_FEED_ITEM])
} else { } else {
@ -65,39 +64,21 @@ export const Feed = observer(function Feed({
} }
} }
return feedItems return feedItems
}, [ }, [feed.hasError, feed.hasLoaded, feed.isEmpty, feed.nonReplyFeed])
feed.hasError,
feed.hasLoaded,
feed.isEmpty,
feed.nonReplyFeed,
showWelcomeBanner,
isNewUser,
])
// events // events
// = // =
const checkWelcome = React.useCallback(async () => {
if (showWelcomeBanner && store.me.did) {
await store.me.follows.fetchIfNeeded()
setIsNewUser(store.me.follows.isEmpty)
}
}, [showWelcomeBanner, store.me.follows, store.me.did])
React.useEffect(() => {
checkWelcome()
}, [checkWelcome])
const onRefresh = React.useCallback(async () => { const onRefresh = React.useCallback(async () => {
track('Feed:onRefresh') track('Feed:onRefresh')
setIsRefreshing(true) setIsRefreshing(true)
checkWelcome()
try { try {
await feed.refresh() await feed.refresh()
} catch (err) { } catch (err) {
feed.rootStore.log.error('Failed to refresh posts feed', err) feed.rootStore.log.error('Failed to refresh posts feed', err)
} }
setIsRefreshing(false) setIsRefreshing(false)
}, [feed, track, setIsRefreshing, checkWelcome]) }, [feed, track, setIsRefreshing])
const onEndReached = React.useCallback(async () => { const onEndReached = React.useCallback(async () => {
track('Feed:onEndReached') track('Feed:onEndReached')
try { try {
@ -118,11 +99,30 @@ export const Feed = observer(function Feed({
({item}: {item: any}) => { ({item}: {item: any}) => {
if (item === EMPTY_FEED_ITEM) { if (item === EMPTY_FEED_ITEM) {
return ( return (
<EmptyState <View style={styles.emptyContainer}>
icon="bars" <View style={styles.emptyIconContainer}>
message="This feed is empty!" <MagnifyingGlassIcon
style={styles.emptyState} style={[styles.emptyIcon, pal.text]}
size={62}
/> />
</View>
<Text type="xl-medium" style={[s.textCenter, pal.text]}>
Your feed is empty! You should follow some accounts to fix this.
</Text>
<Button
type="inverted"
style={styles.emptyBtn}
onPress={() => store.nav.navigate('/search')}>
<Text type="lg-medium" style={palInverted.text}>
Find accounts
</Text>
<FontAwesomeIcon
icon="angle-right"
style={palInverted.text as FontAwesomeIconStyle}
size={14}
/>
</Button>
</View>
) )
} else if (item === ERROR_FEED_ITEM) { } else if (item === ERROR_FEED_ITEM) {
return ( return (
@ -131,12 +131,10 @@ export const Feed = observer(function Feed({
onPressTryAgain={onPressTryAgain} onPressTryAgain={onPressTryAgain}
/> />
) )
} else if (item === WELCOME_FEED_ITEM) {
return <WelcomeBanner />
} }
return <FeedItem item={item} showFollowBtn={showPostFollowBtn} /> return <FeedItem item={item} showFollowBtn={showPostFollowBtn} />
}, },
[feed, onPressTryAgain, showPostFollowBtn], [feed, onPressTryAgain, showPostFollowBtn, pal, palInverted, store.nav],
) )
const FeedFooter = React.useCallback( const FeedFooter = React.useCallback(
@ -155,7 +153,6 @@ export const Feed = observer(function Feed({
<View testID={testID} style={style}> <View testID={testID} style={style}>
{feed.isLoading && data.length === 0 && ( {feed.isLoading && data.length === 0 && (
<CenteredView style={{paddingTop: headerOffset}}> <CenteredView style={{paddingTop: headerOffset}}>
{showWelcomeBanner && isNewUser && <WelcomeBanner />}
<PostFeedLoadingPlaceholder /> <PostFeedLoadingPlaceholder />
</CenteredView> </CenteredView>
)} )}
@ -184,5 +181,21 @@ export const Feed = observer(function Feed({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
feedFooter: {paddingTop: 20}, feedFooter: {paddingTop: 20},
emptyState: {paddingVertical: 40}, emptyContainer: {
paddingVertical: 40,
paddingHorizontal: 30,
},
emptyIconContainer: {
marginBottom: 16,
},
emptyIcon: {
marginLeft: 'auto',
marginRight: 'auto',
},
emptyBtn: {
marginTop: 20,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
}) })

View File

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

View File

@ -24,20 +24,18 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
let handle = opts.authorHandle let handle = opts.authorHandle
const store = useStores() const store = useStores()
const isMe = opts.did === store.me.did const isMe = opts.did === store.me.did
const isFollowing =
typeof opts.did === 'string' && store.me.follows.isFollowing(opts.did)
// NOTE we capture `isFollowing` via a memo so that follows const [didFollow, setDidFollow] = React.useState(false)
// don't change this UI immediately, but rather upon future const onToggleFollow = React.useCallback(() => {
// renders setDidFollow(true)
const isFollowing = React.useMemo( }, [setDidFollow])
() =>
typeof opts.did === 'string' && store.me.follows.isFollowing(opts.did),
[opts.did, store.me.follows],
)
if ( if (
opts.showFollowBtn && opts.showFollowBtn &&
!isMe && !isMe &&
!isFollowing && (!isFollowing || didFollow) &&
opts.did && opts.did &&
opts.declarationCid opts.declarationCid
) { ) {
@ -71,7 +69,11 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
</View> </View>
<View> <View>
<FollowButton did={opts.did} declarationCid={opts.declarationCid} /> <FollowButton
did={opts.did}
declarationCid={opts.declarationCid}
onToggleFollow={onToggleFollow}
/>
</View> </View>
</View> </View>
) )

View File

@ -1,11 +1,43 @@
import React from 'react' import React from 'react'
import {StyleSheet, View} from 'react-native' import {StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {Text} from './text/Text' import {Text} from './text/Text'
import {Button} from './forms/Button'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {useStores} from 'state/index'
import {SUGGESTED_FOLLOWS} from 'lib/constants'
// @ts-ignore no type definition -prf
import ProgressBar from 'react-native-progress/Bar'
export function WelcomeBanner() { export const WelcomeBanner = observer(() => {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores()
const [isReady, setIsReady] = React.useState(false)
const numFollows = Math.min(
SUGGESTED_FOLLOWS(String(store.agent.service)).length,
5,
)
const remaining = numFollows - store.me.follows.numFollows
React.useEffect(() => {
if (remaining <= 0) {
// wait 500ms for the progress bar anim to finish
const ti = setTimeout(() => {
setIsReady(true)
}, 500)
return () => clearTimeout(ti)
} else {
setIsReady(false)
}
}, [remaining])
const onPressDone = React.useCallback(() => {
store.shell.setOnboarding(false)
}, [store])
return ( return (
<View <View
testID="welcomeBanner" testID="welcomeBanner"
@ -16,18 +48,53 @@ export function WelcomeBanner() {
lineHeight={1.1}> lineHeight={1.1}>
Welcome to the private beta! Welcome to the private beta!
</Text> </Text>
<Text type="lg" style={[pal.text, s.textCenter]}> {isReady ? (
Here are some recent posts. Follow their creators to build your feed. <View style={styles.controls}>
<Button
type="primary"
style={[s.flexRow, s.alignCenter]}
onPress={onPressDone}>
<Text type="md-bold" style={s.white}>
See my feed!
</Text> </Text>
<FontAwesomeIcon icon="angle-right" size={14} style={s.white} />
</Button>
</View>
) : (
<>
<Text type="lg" style={[pal.text, s.textCenter]}>
Follow at least {remaining} {remaining === 1 ? 'person' : 'people'}{' '}
to build your feed.
</Text>
<View style={[styles.controls, styles.progress]}>
<ProgressBar
progress={Math.max(
store.me.follows.numFollows / numFollows,
0.05,
)}
/>
</View>
</>
)}
</View> </View>
) )
} })
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
paddingTop: 30, paddingTop: 16,
paddingBottom: 26, paddingBottom: 16,
paddingHorizontal: 20, paddingHorizontal: 20,
borderTopWidth: 1, borderTopWidth: 1,
borderBottomWidth: 1,
},
controls: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginTop: 10,
},
progress: {
marginTop: 12,
}, },
}) })

View File

@ -13,6 +13,7 @@ import {choose} from 'lib/functions'
export type ButtonType = export type ButtonType =
| 'primary' | 'primary'
| 'secondary' | 'secondary'
| 'default'
| 'inverted' | 'inverted'
| 'primary-outline' | 'primary-outline'
| 'secondary-outline' | 'secondary-outline'
@ -40,6 +41,9 @@ export function Button({
secondary: { secondary: {
backgroundColor: theme.palette.secondary.background, backgroundColor: theme.palette.secondary.background,
}, },
default: {
backgroundColor: theme.palette.default.backgroundLight,
},
inverted: { inverted: {
backgroundColor: theme.palette.inverted.background, backgroundColor: theme.palette.inverted.background,
}, },
@ -66,15 +70,18 @@ export function Button({
const labelStyle = choose<TextStyle, Record<ButtonType, TextStyle>>(type, { const labelStyle = choose<TextStyle, Record<ButtonType, TextStyle>>(type, {
primary: { primary: {
color: theme.palette.primary.text, color: theme.palette.primary.text,
fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined, fontWeight: '600',
}, },
secondary: { secondary: {
color: theme.palette.secondary.text, color: theme.palette.secondary.text,
fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined, fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
}, },
default: {
color: theme.palette.default.text,
},
inverted: { inverted: {
color: theme.palette.inverted.text, color: theme.palette.inverted.text,
fontWeight: theme.palette.inverted.isLowContrast ? '500' : undefined, fontWeight: '600',
}, },
'primary-outline': { 'primary-outline': {
color: theme.palette.primary.textInverted, color: theme.palette.primary.textInverted,
@ -114,7 +121,8 @@ export function Button({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
outer: { outer: {
paddingHorizontal: 10, paddingHorizontal: 14,
paddingVertical: 8, paddingVertical: 8,
borderRadius: 24,
}, },
}) })

View File

@ -341,6 +341,9 @@ function ButtonsView() {
<View style={[s.flexRow, s.mb5]}> <View style={[s.flexRow, s.mb5]}>
<Button type="primary" label="Primary solid" style={buttonStyles} /> <Button type="primary" label="Primary solid" style={buttonStyles} />
<Button type="secondary" label="Secondary solid" style={buttonStyles} /> <Button type="secondary" label="Secondary solid" style={buttonStyles} />
</View>
<View style={[s.flexRow, s.mb5]}>
<Button type="default" label="Default solid" style={buttonStyles} />
<Button type="inverted" label="Inverted solid" style={buttonStyles} /> <Button type="inverted" label="Inverted solid" style={buttonStyles} />
</View> </View>
<View style={s.flexRow}> <View style={s.flexRow}>

View File

@ -1,10 +1,11 @@
import React, {useEffect} from 'react' import React from 'react'
import {FlatList, View} from 'react-native' import {FlatList, View} from 'react-native'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import useAppState from 'react-native-appstate-hook' import useAppState from 'react-native-appstate-hook'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {Feed} from '../com/posts/Feed' import {Feed} from '../com/posts/Feed'
import {LoadLatestBtn} from '../com/util/LoadLatestBtn' import {LoadLatestBtn} from '../com/util/LoadLatestBtn'
import {WelcomeBanner} from '../com/util/WelcomeBanner'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {ScreenParams} from '../routes' import {ScreenParams} from '../routes'
import {s} from 'lib/styles' import {s} from 'lib/styles'
@ -43,7 +44,7 @@ export const Home = observer(function Home({navIdx, visible}: ScreenParams) {
scrollElRef.current?.scrollToOffset({offset: -HEADER_HEIGHT}) scrollElRef.current?.scrollToOffset({offset: -HEADER_HEIGHT})
}, [scrollElRef]) }, [scrollElRef])
useEffect(() => { React.useEffect(() => {
const softResetSub = store.onScreenSoftReset(scrollToTop) const softResetSub = store.onScreenSoftReset(scrollToTop)
const feedCleanup = store.me.mainFeed.registerListeners() const feedCleanup = store.me.mainFeed.registerListeners()
const pollInterval = setInterval(doPoll, 15e3) const pollInterval = setInterval(doPoll, 15e3)
@ -72,7 +73,16 @@ export const Home = observer(function Home({navIdx, visible}: ScreenParams) {
store.me.mainFeed.update() store.me.mainFeed.update()
} }
return cleanup return cleanup
}, [visible, store, store.me.mainFeed, navIdx, doPoll, wasVisible, scrollToTop, screen]) }, [
visible,
store,
store.me.mainFeed,
navIdx,
doPoll,
wasVisible,
scrollToTop,
screen,
])
const onPressTryAgain = () => { const onPressTryAgain = () => {
store.me.mainFeed.refresh() store.me.mainFeed.refresh()
@ -84,19 +94,21 @@ export const Home = observer(function Home({navIdx, visible}: ScreenParams) {
return ( return (
<View style={s.hContentRegion}> <View style={s.hContentRegion}>
{store.shell.isOnboarding && <WelcomeBanner />}
<Feed <Feed
testID="homeFeed" testID="homeFeed"
key="default" key="default"
feed={store.me.mainFeed} feed={store.me.mainFeed}
scrollElRef={scrollElRef} scrollElRef={scrollElRef}
style={s.hContentRegion} style={s.hContentRegion}
showWelcomeBanner
showPostFollowBtn showPostFollowBtn
onPressTryAgain={onPressTryAgain} onPressTryAgain={onPressTryAgain}
onScroll={onMainScroll} onScroll={onMainScroll}
headerOffset={HEADER_HEIGHT} headerOffset={store.shell.isOnboarding ? 0 : HEADER_HEIGHT}
/> />
{!store.shell.isOnboarding && (
<ViewHeader title="Bluesky" canGoBack={false} hideOnScroll /> <ViewHeader title="Bluesky" canGoBack={false} hideOnScroll />
)}
{store.me.mainFeed.hasNewLatest && !store.me.mainFeed.isRefreshing && ( {store.me.mainFeed.hasNewLatest && !store.me.mainFeed.isRefreshing && (
<LoadLatestBtn onPress={onPressLoadLatest} /> <LoadLatestBtn onPress={onPressLoadLatest} />
)} )}

View File

@ -131,14 +131,10 @@ export const Menu = observer(({onClose}: {onClose: () => void}) => {
/> />
<Text <Text
type="title-lg" type="title-lg"
style={[pal.text, s.bold, styles.profileCardDisplayName]} style={[pal.text, s.bold, styles.profileCardDisplayName]}>
numberOfLines={1}>
{store.me.displayName || store.me.handle} {store.me.displayName || store.me.handle}
</Text> </Text>
<Text <Text type="2xl" style={[pal.textLight, styles.profileCardHandle]}>
type="2xl"
style={[pal.textLight, styles.profileCardHandle]}
numberOfLines={1}>
@{store.me.handle} @{store.me.handle}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
@ -280,9 +276,11 @@ const styles = StyleSheet.create({
profileCardDisplayName: { profileCardDisplayName: {
marginTop: 20, marginTop: 20,
paddingRight: 20,
}, },
profileCardHandle: { profileCardHandle: {
marginTop: 4, marginTop: 4,
paddingRight: 20,
}, },
menuItem: { menuItem: {