about summary refs log tree commit diff
path: root/src/view
diff options
context:
space:
mode:
Diffstat (limited to 'src/view')
-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
8 files changed, 194 insertions, 95 deletions
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: {