about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib/api/build-suggested-posts.ts21
-rw-r--r--src/state/models/feed-view.ts57
-rw-r--r--src/state/models/my-follows.ts4
-rw-r--r--src/state/models/session.ts1
-rw-r--r--src/state/models/shell-ui.ts10
-rw-r--r--src/view/com/posts/Feed.tsx95
-rw-r--r--src/view/com/profile/FollowButton.tsx40
-rw-r--r--src/view/com/util/PostMeta.tsx22
-rw-r--r--src/view/com/util/WelcomeBanner.tsx81
-rw-r--r--src/view/com/util/forms/Button.tsx14
-rw-r--r--src/view/screens/Debug.tsx3
-rw-r--r--src/view/screens/Home.tsx24
-rw-r--r--src/view/shell/mobile/Menu.tsx10
13 files changed, 259 insertions, 123 deletions
diff --git a/src/lib/api/build-suggested-posts.ts b/src/lib/api/build-suggested-posts.ts
index 6250f4a9c..defa45311 100644
--- a/src/lib/api/build-suggested-posts.ts
+++ b/src/lib/api/build-suggested-posts.ts
@@ -37,13 +37,20 @@ function mergePosts(
         // filter the feed down to the post with the most upvotes
         res.data.feed = res.data.feed.reduce(
           (acc: AppBskyFeedFeedViewPost.Main[], v) => {
-            if (!acc?.[0] && !v.reason) {
+            if (
+              !acc?.[0] &&
+              !v.reason &&
+              !v.reply &&
+              isRecentEnough(v.post.indexedAt)
+            ) {
               return [v]
             }
             if (
               acc &&
               !v.reason &&
-              v.post.upvoteCount > acc[0].post.upvoteCount
+              !v.reply &&
+              v.post.upvoteCount > acc[0]?.post.upvoteCount &&
+              isRecentEnough(v.post.indexedAt)
             ) {
               return [v]
             }
@@ -112,6 +119,16 @@ function isCombinedCursor(cursor: string) {
   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 {
   getMultipleAuthorsPosts,
   mergePosts,
diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts
index 535221e63..e27712d11 100644
--- a/src/state/models/feed-view.ts
+++ b/src/state/models/feed-view.ts
@@ -212,7 +212,7 @@ export class FeedModel {
 
   constructor(
     public rootStore: RootStoreModel,
-    public feedType: 'home' | 'author',
+    public feedType: 'home' | 'author' | 'suggested',
     params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams,
   ) {
     makeAutoObservable(
@@ -256,7 +256,7 @@ export class FeedModel {
             item.reply?.root.author.did === item.post.author.did)
         )
       })
-    } else {
+    } else if (this.feedType === 'home') {
       return this.feed.filter(item => {
         const isRepost = Boolean(item?.reasonRepost)
         return (
@@ -267,6 +267,8 @@ export class FeedModel {
           item.post.upvoteCount >= 2
         )
       })
+    } else {
+      return this.feed
     }
   }
 
@@ -293,6 +295,14 @@ export class FeedModel {
     this.feed = []
   }
 
+  switchFeedType(feedType: 'home' | 'suggested') {
+    if (this.feedType === feedType) {
+      return
+    }
+    this.feedType = feedType
+    return this.setup()
+  }
+
   /**
    * Load for first render
    */
@@ -427,7 +437,7 @@ export class FeedModel {
    * Check if new posts are available
    */
   async checkForLatest() {
-    if (this.hasNewLatest || this.rootStore.me.follows.isEmpty) {
+    if (this.hasNewLatest || this.feedType === 'suggested') {
       return
     }
     const res = await this._getFeed({limit: 1})
@@ -562,30 +572,25 @@ export class FeedModel {
     params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams = {},
   ): Promise<GetTimeline.Response | GetAuthorFeed.Response> {
     params = Object.assign({}, this.params, params)
-    if (this.feedType === 'home') {
-      await this.rootStore.me.follows.fetchIfNeeded()
-      if (this.rootStore.me.follows.isEmpty) {
-        const responses = await getMultipleAuthorsPosts(
-          this.rootStore,
-          sampleSize(
-            SUGGESTED_FOLLOWS(String(this.rootStore.agent.service)),
-            20,
-          ),
-          params.before,
-          20,
-        )
-        const combinedCursor = getCombinedCursors(responses)
-        const finalData = mergePosts(responses, {bestOfOnly: true})
-        const lastHeaders = responses[responses.length - 1].headers
-        return {
-          success: true,
-          data: {
-            feed: finalData,
-            cursor: combinedCursor,
-          },
-          headers: lastHeaders,
-        }
+    if (this.feedType === 'suggested') {
+      const responses = await getMultipleAuthorsPosts(
+        this.rootStore,
+        sampleSize(SUGGESTED_FOLLOWS(String(this.rootStore.agent.service)), 20),
+        params.before,
+        20,
+      )
+      const combinedCursor = getCombinedCursors(responses)
+      const finalData = mergePosts(responses, {bestOfOnly: true})
+      const lastHeaders = responses[responses.length - 1].headers
+      return {
+        success: true,
+        data: {
+          feed: finalData,
+          cursor: combinedCursor,
+        },
+        headers: lastHeaders,
       }
+    } else if (this.feedType === 'home') {
       return this.rootStore.api.app.bsky.feed.getTimeline(
         params as GetTimeline.QueryParams,
       )
diff --git a/src/state/models/my-follows.ts b/src/state/models/my-follows.ts
index c1fba1352..732c2fe73 100644
--- a/src/state/models/my-follows.ts
+++ b/src/state/models/my-follows.ts
@@ -72,6 +72,10 @@ export class MyFollowsModel {
     return !!this.followDidToRecordMap[did]
   }
 
+  get numFollows() {
+    return Object.keys(this.followDidToRecordMap).length
+  }
+
   get isEmpty() {
     return Object.keys(this.followDidToRecordMap).length === 0
   }
diff --git a/src/state/models/session.ts b/src/state/models/session.ts
index 75a60f353..b15c866f4 100644
--- a/src/state/models/session.ts
+++ b/src/state/models/session.ts
@@ -345,6 +345,7 @@ export class SessionModel {
     )
 
     this.setActiveSession(agent, did)
+    this.rootStore.shell.setOnboarding(true)
     this.rootStore.log.debug('SessionModel:createAccount succeeded')
   }
 
diff --git a/src/state/models/shell-ui.ts b/src/state/models/shell-ui.ts
index 1b0e350a2..0dad2bd9e 100644
--- a/src/state/models/shell-ui.ts
+++ b/src/state/models/shell-ui.ts
@@ -118,6 +118,7 @@ export class ShellUiModel {
   activeLightbox: ProfileImageLightbox | ImagesLightbox | undefined
   isComposerActive = false
   composerOpts: ComposerOpts | undefined
+  isOnboarding = false
 
   constructor(public rootStore: RootStoreModel) {
     makeAutoObservable(this, {
@@ -185,4 +186,13 @@ export class ShellUiModel {
     this.isComposerActive = false
     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')
+    }
+  }
 }
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx
index 7ed6bc711..f919c6208 100644
--- a/src/view/com/posts/Feed.tsx
+++ b/src/view/com/posts/Feed.tsx
@@ -7,26 +7,28 @@ import {
   StyleSheet,
   ViewStyle,
 } from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
 import {CenteredView, FlatList} from '../util/Views'
 import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
-import {EmptyState} from '../util/EmptyState'
+import {Text} from '../util/text/Text'
 import {ErrorMessage} from '../util/error/ErrorMessage'
+import {Button} from '../util/forms/Button'
 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'
+import {useAnalytics} from 'lib/analytics'
+import {usePalette} from 'lib/hooks/usePalette'
+import {MagnifyingGlassIcon} from 'lib/icons'
 
 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,
@@ -36,7 +38,6 @@ export const Feed = observer(function Feed({
 }: {
   feed: FeedModel
   style?: StyleProp<ViewStyle>
-  showWelcomeBanner?: boolean
   showPostFollowBtn?: boolean
   scrollElRef?: MutableRefObject<FlatList<any> | null>
   onPressTryAgain?: () => void
@@ -44,10 +45,11 @@ export const Feed = observer(function Feed({
   testID?: string
   headerOffset?: number
 }) {
-  const {track} = useAnalytics()
+  const pal = usePalette('default')
+  const palInverted = usePalette('inverted')
   const store = useStores()
+  const {track} = useAnalytics()
   const [isRefreshing, setIsRefreshing] = React.useState(false)
-  const [isNewUser, setIsNewUser] = React.useState<boolean>(false)
 
   const data = React.useMemo(() => {
     let feedItems: any[] = []
@@ -55,9 +57,6 @@ 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 {
@@ -65,39 +64,21 @@ export const Feed = observer(function Feed({
       }
     }
     return feedItems
-  }, [
-    feed.hasError,
-    feed.hasLoaded,
-    feed.isEmpty,
-    feed.nonReplyFeed,
-    showWelcomeBanner,
-    isNewUser,
-  ])
+  }, [feed.hasError, feed.hasLoaded, feed.isEmpty, feed.nonReplyFeed])
 
   // 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 () => {
     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, checkWelcome])
+  }, [feed, track, setIsRefreshing])
   const onEndReached = React.useCallback(async () => {
     track('Feed:onEndReached')
     try {
@@ -118,11 +99,30 @@ export const Feed = observer(function Feed({
     ({item}: {item: any}) => {
       if (item === EMPTY_FEED_ITEM) {
         return (
-          <EmptyState
-            icon="bars"
-            message="This feed is empty!"
-            style={styles.emptyState}
-          />
+          <View style={styles.emptyContainer}>
+            <View style={styles.emptyIconContainer}>
+              <MagnifyingGlassIcon
+                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) {
         return (
@@ -131,12 +131,10 @@ export const Feed = observer(function Feed({
             onPressTryAgain={onPressTryAgain}
           />
         )
-      } else if (item === WELCOME_FEED_ITEM) {
-        return <WelcomeBanner />
       }
       return <FeedItem item={item} showFollowBtn={showPostFollowBtn} />
     },
-    [feed, onPressTryAgain, showPostFollowBtn],
+    [feed, onPressTryAgain, showPostFollowBtn, pal, palInverted, store.nav],
   )
 
   const FeedFooter = React.useCallback(
@@ -155,7 +153,6 @@ 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>
       )}
@@ -184,5 +181,21 @@ export const Feed = observer(function Feed({
 
 const styles = StyleSheet.create({
   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',
+  },
 })
diff --git a/src/view/com/profile/FollowButton.tsx b/src/view/com/profile/FollowButton.tsx
index 71462bea8..f24c3d0c9 100644
--- a/src/view/com/profile/FollowButton.tsx
+++ b/src/view/com/profile/FollowButton.tsx
@@ -1,23 +1,29 @@
 import React from 'react'
-import {StyleSheet, TouchableOpacity, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
-import {Text} from '../util/text/Text'
+import {Button} from '../util/forms/Button'
 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}) => {
+  ({
+    did,
+    declarationCid,
+    onToggleFollow,
+  }: {
+    did: string
+    declarationCid: string
+    onToggleFollow?: (v: boolean) => void
+  }) => {
     const store = useStores()
-    const pal = usePalette('default')
     const isFollowing = store.me.follows.isFollowing(did)
 
-    const onToggleFollow = async () => {
+    const onToggleFollowInner = async () => {
       if (store.me.follows.isFollowing(did)) {
         try {
           await apilib.unfollow(store, store.me.follows.getFollowUri(did))
           store.me.follows.removeFollow(did)
+          onToggleFollow?.(false)
         } catch (e: any) {
           store.log.error('Failed fo delete follow', e)
           Toast.show('An issue occurred, please try again.')
@@ -26,6 +32,7 @@ const FollowButton = observer(
         try {
           const res = await apilib.follow(store, did, declarationCid)
           store.me.follows.addFollow(did, res.uri)
+          onToggleFollow?.(true)
         } catch (e: any) {
           store.log.error('Failed fo create follow', e)
           Toast.show('An issue occurred, please try again.')
@@ -34,24 +41,13 @@ const FollowButton = observer(
     }
 
     return (
-      <TouchableOpacity onPress={onToggleFollow}>
-        <View style={[styles.btn, pal.btn]}>
-          <Text type="button" style={[pal.text]}>
-            {isFollowing ? 'Unfollow' : 'Follow'}
-          </Text>
-        </View>
-      </TouchableOpacity>
+      <Button
+        type={isFollowing ? 'default' : 'primary'}
+        onPress={onToggleFollowInner}
+        label={isFollowing ? 'Unfollow' : 'Follow'}
+      />
     )
   },
 )
 
 export default FollowButton
-
-const styles = StyleSheet.create({
-  btn: {
-    paddingVertical: 7,
-    borderRadius: 50,
-    marginLeft: 6,
-    paddingHorizontal: 14,
-  },
-})
diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx
index af08708b4..cde5a3e92 100644
--- a/src/view/com/util/PostMeta.tsx
+++ b/src/view/com/util/PostMeta.tsx
@@ -24,20 +24,18 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
   let handle = opts.authorHandle
   const store = useStores()
   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
-  //      don't change this UI immediately, but rather upon future
-  //      renders
-  const isFollowing = React.useMemo(
-    () =>
-      typeof opts.did === 'string' && store.me.follows.isFollowing(opts.did),
-    [opts.did, store.me.follows],
-  )
+  const [didFollow, setDidFollow] = React.useState(false)
+  const onToggleFollow = React.useCallback(() => {
+    setDidFollow(true)
+  }, [setDidFollow])
 
   if (
     opts.showFollowBtn &&
     !isMe &&
-    !isFollowing &&
+    (!isFollowing || didFollow) &&
     opts.did &&
     opts.declarationCid
   ) {
@@ -71,7 +69,11 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
         </View>
 
         <View>
-          <FollowButton did={opts.did} declarationCid={opts.declarationCid} />
+          <FollowButton
+            did={opts.did}
+            declarationCid={opts.declarationCid}
+            onToggleFollow={onToggleFollow}
+          />
         </View>
       </View>
     )
diff --git a/src/view/com/util/WelcomeBanner.tsx b/src/view/com/util/WelcomeBanner.tsx
index d52288502..e236bfb48 100644
--- a/src/view/com/util/WelcomeBanner.tsx
+++ b/src/view/com/util/WelcomeBanner.tsx
@@ -1,11 +1,43 @@
 import React from 'react'
 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 {Text} from './text/Text'
+import {Button} from './forms/Button'
 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 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 (
     <View
       testID="welcomeBanner"
@@ -16,18 +48,53 @@ export function WelcomeBanner() {
         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>
+      {isReady ? (
+        <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>
+            <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>
   )
-}
+})
 
 const styles = StyleSheet.create({
   container: {
-    paddingTop: 30,
-    paddingBottom: 26,
+    paddingTop: 16,
+    paddingBottom: 16,
     paddingHorizontal: 20,
     borderTopWidth: 1,
+    borderBottomWidth: 1,
+  },
+  controls: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    marginTop: 10,
+  },
+  progress: {
+    marginTop: 12,
   },
 })
diff --git a/src/view/com/util/forms/Button.tsx b/src/view/com/util/forms/Button.tsx
index a070d2f0f..f3f4d1c79 100644
--- a/src/view/com/util/forms/Button.tsx
+++ b/src/view/com/util/forms/Button.tsx
@@ -13,6 +13,7 @@ import {choose} from 'lib/functions'
 export type ButtonType =
   | 'primary'
   | 'secondary'
+  | 'default'
   | 'inverted'
   | 'primary-outline'
   | 'secondary-outline'
@@ -40,6 +41,9 @@ export function Button({
     secondary: {
       backgroundColor: theme.palette.secondary.background,
     },
+    default: {
+      backgroundColor: theme.palette.default.backgroundLight,
+    },
     inverted: {
       backgroundColor: theme.palette.inverted.background,
     },
@@ -66,15 +70,18 @@ export function Button({
   const labelStyle = choose<TextStyle, Record<ButtonType, TextStyle>>(type, {
     primary: {
       color: theme.palette.primary.text,
-      fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
+      fontWeight: '600',
     },
     secondary: {
       color: theme.palette.secondary.text,
       fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
     },
+    default: {
+      color: theme.palette.default.text,
+    },
     inverted: {
       color: theme.palette.inverted.text,
-      fontWeight: theme.palette.inverted.isLowContrast ? '500' : undefined,
+      fontWeight: '600',
     },
     'primary-outline': {
       color: theme.palette.primary.textInverted,
@@ -114,7 +121,8 @@ export function Button({
 
 const styles = StyleSheet.create({
   outer: {
-    paddingHorizontal: 10,
+    paddingHorizontal: 14,
     paddingVertical: 8,
+    borderRadius: 24,
   },
 })
diff --git a/src/view/screens/Debug.tsx b/src/view/screens/Debug.tsx
index 657f38d57..f2349195e 100644
--- a/src/view/screens/Debug.tsx
+++ b/src/view/screens/Debug.tsx
@@ -341,6 +341,9 @@ function ButtonsView() {
       <View style={[s.flexRow, s.mb5]}>
         <Button type="primary" label="Primary 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} />
       </View>
       <View style={s.flexRow}>
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index b9611757c..09006a27f 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -1,10 +1,11 @@
-import React, {useEffect} from 'react'
+import React from 'react'
 import {FlatList, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
 import useAppState from 'react-native-appstate-hook'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {Feed} from '../com/posts/Feed'
 import {LoadLatestBtn} from '../com/util/LoadLatestBtn'
+import {WelcomeBanner} from '../com/util/WelcomeBanner'
 import {useStores} from 'state/index'
 import {ScreenParams} from '../routes'
 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])
 
-  useEffect(() => {
+  React.useEffect(() => {
     const softResetSub = store.onScreenSoftReset(scrollToTop)
     const feedCleanup = store.me.mainFeed.registerListeners()
     const pollInterval = setInterval(doPoll, 15e3)
@@ -72,7 +73,16 @@ export const Home = observer(function Home({navIdx, visible}: ScreenParams) {
       store.me.mainFeed.update()
     }
     return cleanup
-  }, [visible, store, store.me.mainFeed, navIdx, doPoll, wasVisible, scrollToTop, screen])
+  }, [
+    visible,
+    store,
+    store.me.mainFeed,
+    navIdx,
+    doPoll,
+    wasVisible,
+    scrollToTop,
+    screen,
+  ])
 
   const onPressTryAgain = () => {
     store.me.mainFeed.refresh()
@@ -84,19 +94,21 @@ export const Home = observer(function Home({navIdx, visible}: ScreenParams) {
 
   return (
     <View style={s.hContentRegion}>
+      {store.shell.isOnboarding && <WelcomeBanner />}
       <Feed
         testID="homeFeed"
         key="default"
         feed={store.me.mainFeed}
         scrollElRef={scrollElRef}
         style={s.hContentRegion}
-        showWelcomeBanner
         showPostFollowBtn
         onPressTryAgain={onPressTryAgain}
         onScroll={onMainScroll}
-        headerOffset={HEADER_HEIGHT}
+        headerOffset={store.shell.isOnboarding ? 0 : HEADER_HEIGHT}
       />
-      <ViewHeader title="Bluesky" canGoBack={false} hideOnScroll />
+      {!store.shell.isOnboarding && (
+        <ViewHeader title="Bluesky" canGoBack={false} hideOnScroll />
+      )}
       {store.me.mainFeed.hasNewLatest && !store.me.mainFeed.isRefreshing && (
         <LoadLatestBtn onPress={onPressLoadLatest} />
       )}
diff --git a/src/view/shell/mobile/Menu.tsx b/src/view/shell/mobile/Menu.tsx
index 6c5aa1adb..734e02b08 100644
--- a/src/view/shell/mobile/Menu.tsx
+++ b/src/view/shell/mobile/Menu.tsx
@@ -131,14 +131,10 @@ export const Menu = observer(({onClose}: {onClose: () => void}) => {
         />
         <Text
           type="title-lg"
-          style={[pal.text, s.bold, styles.profileCardDisplayName]}
-          numberOfLines={1}>
+          style={[pal.text, s.bold, styles.profileCardDisplayName]}>
           {store.me.displayName || store.me.handle}
         </Text>
-        <Text
-          type="2xl"
-          style={[pal.textLight, styles.profileCardHandle]}
-          numberOfLines={1}>
+        <Text type="2xl" style={[pal.textLight, styles.profileCardHandle]}>
           @{store.me.handle}
         </Text>
       </TouchableOpacity>
@@ -280,9 +276,11 @@ const styles = StyleSheet.create({
 
   profileCardDisplayName: {
     marginTop: 20,
+    paddingRight: 20,
   },
   profileCardHandle: {
     marginTop: 4,
+    paddingRight: 20,
   },
 
   menuItem: {