about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/state/models/feed-view.ts12
-rw-r--r--src/view/com/profile/FollowButton.tsx2
-rw-r--r--src/view/com/util/Pager.tsx27
-rw-r--r--src/view/com/util/PostMeta.tsx8
-rw-r--r--src/view/com/util/TabBar.tsx25
-rw-r--r--src/view/screens/Home.tsx360
6 files changed, 272 insertions, 162 deletions
diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts
index 42b753b24..81760132e 100644
--- a/src/state/models/feed-view.ts
+++ b/src/state/models/feed-view.ts
@@ -257,7 +257,7 @@ export class FeedModel {
 
   constructor(
     public rootStore: RootStoreModel,
-    public feedType: 'home' | 'author' | 'suggested',
+    public feedType: 'home' | 'author' | 'suggested' | 'goodstuff',
     params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams,
   ) {
     makeAutoObservable(
@@ -634,6 +634,16 @@ export class FeedModel {
       return this.rootStore.api.app.bsky.feed.getTimeline(
         params as GetTimeline.QueryParams,
       )
+    } else if (this.feedType === 'goodstuff') {
+      const res = await this.rootStore.api.app.bsky.feed.getAuthorFeed({
+        ...params,
+        author: 'jay.bsky.social',
+      } as GetAuthorFeed.QueryParams)
+      res.data.feed = mergePosts([res], {repostsOnly: true})
+      res.data.feed.forEach(item => {
+        delete item.reason
+      })
+      return res
     } else {
       return this.rootStore.api.app.bsky.feed.getAuthorFeed(
         params as GetAuthorFeed.QueryParams,
diff --git a/src/view/com/profile/FollowButton.tsx b/src/view/com/profile/FollowButton.tsx
index f24c3d0c9..7a194cee9 100644
--- a/src/view/com/profile/FollowButton.tsx
+++ b/src/view/com/profile/FollowButton.tsx
@@ -42,7 +42,7 @@ const FollowButton = observer(
 
     return (
       <Button
-        type={isFollowing ? 'default' : 'primary'}
+        type={isFollowing ? 'default' : 'inverted'}
         onPress={onToggleFollowInner}
         label={isFollowing ? 'Unfollow' : 'Follow'}
       />
diff --git a/src/view/com/util/Pager.tsx b/src/view/com/util/Pager.tsx
index 1a3ff642c..9ce5006cd 100644
--- a/src/view/com/util/Pager.tsx
+++ b/src/view/com/util/Pager.tsx
@@ -2,16 +2,25 @@ import React from 'react'
 import {Animated, StyleSheet, View} from 'react-native'
 import PagerView, {PagerViewOnPageSelectedEvent} from 'react-native-pager-view'
 import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
-import {TabBar} from './TabBar'
+import {s} from 'lib/styles'
 
 export type PageSelectedEvent = PagerViewOnPageSelectedEvent
 const AnimatedPagerView = Animated.createAnimatedComponent(PagerView)
 
+export interface TabBarProps {
+  selectedPage: number
+  position: Animated.Value
+  offset: Animated.Value
+  onSelect?: (index: number) => void
+}
+
 interface Props {
+  renderTabBar: (props: TabBarProps) => JSX.Element
   onPageSelected?: (e: PageSelectedEvent) => void
 }
 export const Pager = ({
   children,
+  renderTabBar,
   onPageSelected,
 }: React.PropsWithChildren<Props>) => {
   const [selectedPage, setSelectedPage] = React.useState(0)
@@ -36,16 +45,10 @@ export const Pager = ({
 
   return (
     <View>
-      <TabBar
-        position={position}
-        offset={offset}
-        items={['One', 'Two', 'Three']}
-        selectedPage={selectedPage}
-        onSelect={onTabBarSelect}
-      />
+      {renderTabBar({selectedPage, position, offset, onSelect: onTabBarSelect})}
       <AnimatedPagerView
         ref={pagerView}
-        style={{height: '100%'}}
+        style={s.h100pct}
         initialPage={0}
         onPageSelected={onPageSelectedInner}
         onPageScroll={Animated.event(
@@ -64,9 +67,3 @@ export const Pager = ({
     </View>
   )
 }
-
-const styles = StyleSheet.create({
-  tabBar: {
-    flexDirection: 'row',
-  },
-})
diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx
index 0bb402100..3f9e6935f 100644
--- a/src/view/com/util/PostMeta.tsx
+++ b/src/view/com/util/PostMeta.tsx
@@ -44,7 +44,7 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
     // two-liner with follow button
     return (
       <View style={styles.metaTwoLine}>
-        <View>
+        <View style={styles.metaTwoLineLeft}>
           <View style={styles.metaTwoLineTop}>
             <DesktopWebTextLink
               type="lg-bold"
@@ -69,6 +69,7 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
             type="md"
             style={[styles.metaItem, pal.textLight]}
             lineHeight={1.2}
+            numberOfLines={1}
             text={`@${handle}`}
             href={`/profile/${opts.authorHandle}`}
           />
@@ -134,8 +135,13 @@ const styles = StyleSheet.create({
     flexDirection: 'row',
     alignItems: 'center',
     justifyContent: 'space-between',
+    width: '100%',
     paddingBottom: 2,
   },
+  metaTwoLineLeft: {
+    flex: 1,
+    paddingRight: 40,
+  },
   metaTwoLineTop: {
     flexDirection: 'row',
     alignItems: 'baseline',
diff --git a/src/view/com/util/TabBar.tsx b/src/view/com/util/TabBar.tsx
index 3a823e42c..d9f48577c 100644
--- a/src/view/com/util/TabBar.tsx
+++ b/src/view/com/util/TabBar.tsx
@@ -37,7 +37,7 @@ export function TabBar({
   const panX = Animated.add(position, offset)
 
   const underlineStyle = {
-    backgroundColor: pal.colors.text,
+    backgroundColor: pal.colors.link,
     left: panX.interpolate({
       inputRange: items.map((_item, i) => i),
       outputRange: itemLayouts.map(l => l.x),
@@ -79,11 +79,8 @@ export function TabBar({
           <TouchableWithoutFeedback key={i} onPress={() => onPressItem(i)}>
             <View style={styles.item} ref={itemRefs[i]}>
               <Text
-                style={
-                  selected
-                    ? [styles.labelSelected, pal.text]
-                    : [styles.label, pal.textLight]
-                }>
+                type="xl-medium"
+                style={selected ? pal.text : pal.textLight}>
                 {item}
               </Text>
             </View>
@@ -100,20 +97,14 @@ const styles = StyleSheet.create({
     paddingHorizontal: 14,
   },
   item: {
-    paddingTop: 8,
-    paddingBottom: 12,
-    marginRight: 14,
-    paddingHorizontal: 10,
-  },
-  label: {
-    fontWeight: '600',
-  },
-  labelSelected: {
-    fontWeight: '600',
+    paddingTop: 6,
+    paddingBottom: 14,
+    marginRight: 24,
   },
   underline: {
     position: 'absolute',
-    height: 4,
+    height: 3,
     bottom: 0,
+    borderRadius: 4,
   },
 })
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 6c708e2fd..fa200e931 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -1,15 +1,23 @@
 import React from 'react'
-import {FlatList, StyleSheet, View, useWindowDimensions} from 'react-native'
+import {
+  FlatList,
+  StyleSheet,
+  TouchableOpacity,
+  View,
+  useWindowDimensions,
+} from 'react-native'
 import {useFocusEffect, useIsFocused} from '@react-navigation/native'
 import {observer} from 'mobx-react-lite'
 import useAppState from 'react-native-appstate-hook'
 import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types'
+import {FeedModel} from 'state/models/feed-view'
 import {withAuthRequired} from 'view/com/auth/withAuthRequired'
-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 {UserAvatar} from 'view/com/util/UserAvatar'
+import {TabBar} from 'view/com/util/TabBar'
+import {Pager, PageSelectedEvent, TabBarProps} from 'view/com/util/Pager'
 import {FAB} from '../com/util/FAB'
 import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -18,157 +26,255 @@ import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
 import {useAnalytics} from 'lib/analytics'
 import {ComposeIcon2} from 'lib/icons'
 
-import {Pager, PageSelectedEvent} from 'view/com/util/Pager'
-import {Text} from 'view/com/util/text/Text'
-
-const HEADER_HEIGHT = 42
+const TAB_BAR_HEIGHT = 82
 
 type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
 export const HomeScreen = withAuthRequired((_opts: Props) => {
   const store = useStores()
+  const pal = usePalette('default')
+  const [selectedPage, setSelectedPage] = React.useState(0)
+
+  useFocusEffect(
+    React.useCallback(() => {
+      store.shell.setIsDrawerSwipeDisabled(selectedPage > 0)
+      return () => {
+        store.shell.setIsDrawerSwipeDisabled(false)
+      }
+    }, [store, selectedPage]),
+  )
+
   const onPageSelected = React.useCallback(
     (e: PageSelectedEvent) => {
+      setSelectedPage(e.nativeEvent.position)
       store.shell.setIsDrawerSwipeDisabled(e.nativeEvent.position > 0)
     },
     [store],
   )
 
+  const onPressAvi = React.useCallback(() => {
+    store.shell.openDrawer()
+  }, [store])
+
+  const renderTabBar = React.useCallback(
+    (props: TabBarProps) => {
+      return (
+        <View style={[pal.view, styles.tabBar]}>
+          <TouchableOpacity style={styles.tabBarAvi} onPress={onPressAvi}>
+            <UserAvatar avatar={store.me.avatar} size={32} />
+          </TouchableOpacity>
+          <TabBar items={['Popular', 'Following']} {...props} />
+        </View>
+      )
+    },
+    [store.me.avatar, pal, onPressAvi],
+  )
+
+  return (
+    <Pager onPageSelected={onPageSelected} renderTabBar={renderTabBar}>
+      <AlgoView key="1" />
+      <View key="2">
+        <FollowingView />
+      </View>
+    </Pager>
+  )
+})
+
+const AlgoView = observer(() => {
+  const store = useStores()
+  const onMainScroll = useOnMainScroll(store)
+  const {screen, track} = useAnalytics()
+  const scrollElRef = React.useRef<FlatList>(null)
+  const {appState} = useAppState({
+    onForeground: () => doPoll(true),
+  })
+  const isFocused = useIsFocused()
+  const winDim = useWindowDimensions()
+  const containerStyle = React.useMemo(
+    () => ({height: winDim.height - TAB_BAR_HEIGHT}),
+    [winDim],
+  )
+  const algoFeed = React.useMemo(() => {
+    const feed = new FeedModel(store, 'goodstuff', {})
+    feed.setup()
+    return feed
+  }, [store])
+
+  const doPoll = React.useCallback(
+    (knownActive = false) => {
+      if ((!knownActive && appState !== 'active') || !isFocused) {
+        return
+      }
+      if (algoFeed.isLoading) {
+        return
+      }
+      store.log.debug('HomeScreen: Polling for new posts')
+      algoFeed.checkForLatest()
+    },
+    [appState, isFocused, store, algoFeed],
+  )
+
+  const scrollToTop = React.useCallback(() => {
+    scrollElRef.current?.scrollToOffset({offset: 0})
+  }, [scrollElRef])
+
   useFocusEffect(
     React.useCallback(() => {
+      const softResetSub = store.onScreenSoftReset(scrollToTop)
+      const feedCleanup = algoFeed.registerListeners()
+      const pollInterval = setInterval(doPoll, 15e3)
+
+      screen('Feed')
+      store.log.debug('HomeScreen: Updating feed')
+      if (algoFeed.hasContent) {
+        algoFeed.update()
+      }
+
       return () => {
-        store.shell.setIsDrawerSwipeDisabled(false)
+        clearInterval(pollInterval)
+        softResetSub.remove()
+        feedCleanup()
       }
-    }, [store]),
+    }, [store, doPoll, scrollToTop, screen, algoFeed]),
   )
 
+  const onPressCompose = React.useCallback(() => {
+    track('HomeScreen:PressCompose')
+    store.shell.openComposer({})
+  }, [store, track])
+
+  const onPressTryAgain = React.useCallback(() => {
+    algoFeed.refresh()
+  }, [algoFeed])
+
+  const onPressLoadLatest = React.useCallback(() => {
+    algoFeed.refresh()
+    scrollToTop()
+  }, [algoFeed, scrollToTop])
+
   return (
-    <Pager onPageSelected={onPageSelected}>
-      <View key="1">
-        <MyPage>First page</MyPage>
-      </View>
-      <View key="2">
-        <MyPage>Second page</MyPage>
-      </View>
-      <View key="3">
-        <MyPage>Third page</MyPage>
-      </View>
-    </Pager>
+    <View style={containerStyle}>
+      {store.shell.isOnboarding && <WelcomeBanner />}
+      <Feed
+        testID="homeFeed"
+        key="default"
+        feed={algoFeed}
+        scrollElRef={scrollElRef}
+        style={s.hContentRegion}
+        showPostFollowBtn
+        onPressTryAgain={onPressTryAgain}
+        onScroll={onMainScroll}
+      />
+      {algoFeed.hasNewLatest && !algoFeed.isRefreshing && (
+        <LoadLatestBtn onPress={onPressLoadLatest} />
+      )}
+      <FAB
+        testID="composeFAB"
+        onPress={onPressCompose}
+        icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
+      />
+    </View>
   )
 })
-function MyPage({children}) {
+
+const FollowingView = observer(() => {
+  const store = useStores()
+  const onMainScroll = useOnMainScroll(store)
+  const {screen, track} = useAnalytics()
+  const scrollElRef = React.useRef<FlatList>(null)
+  const {appState} = useAppState({
+    onForeground: () => doPoll(true),
+  })
+  const isFocused = useIsFocused()
+  const winDim = useWindowDimensions()
+  const containerStyle = React.useMemo(
+    () => ({height: winDim.height - TAB_BAR_HEIGHT}),
+    [winDim],
+  )
+
+  const doPoll = React.useCallback(
+    (knownActive = false) => {
+      if ((!knownActive && appState !== 'active') || !isFocused) {
+        return
+      }
+      if (store.me.mainFeed.isLoading) {
+        return
+      }
+      store.log.debug('HomeScreen: Polling for new posts')
+      store.me.mainFeed.checkForLatest()
+    },
+    [appState, isFocused, store],
+  )
+
+  const scrollToTop = React.useCallback(() => {
+    scrollElRef.current?.scrollToOffset({offset: 0})
+  }, [scrollElRef])
+
+  useFocusEffect(
+    React.useCallback(() => {
+      const softResetSub = store.onScreenSoftReset(scrollToTop)
+      const feedCleanup = store.me.mainFeed.registerListeners()
+      const pollInterval = setInterval(doPoll, 15e3)
+
+      screen('Feed')
+      store.log.debug('HomeScreen: Updating feed')
+      if (store.me.mainFeed.hasContent) {
+        store.me.mainFeed.update()
+      }
+
+      return () => {
+        clearInterval(pollInterval)
+        softResetSub.remove()
+        feedCleanup()
+      }
+    }, [store, doPoll, scrollToTop, screen]),
+  )
+
+  const onPressCompose = React.useCallback(() => {
+    track('HomeScreen:PressCompose')
+    store.shell.openComposer({})
+  }, [store, track])
+
+  const onPressTryAgain = React.useCallback(() => {
+    store.me.mainFeed.refresh()
+  }, [store])
+
+  const onPressLoadLatest = React.useCallback(() => {
+    store.me.mainFeed.refresh()
+    scrollToTop()
+  }, [store, scrollToTop])
+
   return (
-    <View
-      style={{
-        flex: 1,
-        justifyContent: 'center',
-        alignItems: 'center',
-        borderWidth: 1,
-        backgroundColor: 'white',
-      }}>
-      <Text>{children}</Text>
+    <View style={containerStyle}>
+      {store.shell.isOnboarding && <WelcomeBanner />}
+      <Feed
+        testID="homeFeed"
+        key="default"
+        feed={store.me.mainFeed}
+        scrollElRef={scrollElRef}
+        style={s.hContentRegion}
+        showPostFollowBtn
+        onPressTryAgain={onPressTryAgain}
+        onScroll={onMainScroll}
+      />
+      {store.me.mainFeed.hasNewLatest && !store.me.mainFeed.isRefreshing && (
+        <LoadLatestBtn onPress={onPressLoadLatest} />
+      )}
+      <FAB
+        testID="composeFAB"
+        onPress={onPressCompose}
+        icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
+      />
     </View>
   )
-}
+})
 
 const styles = StyleSheet.create({
   tabBar: {
     flexDirection: 'row',
+    alignItems: 'center',
+    paddingHorizontal: 18,
+  },
+  tabBarAvi: {
+    marginRight: 16,
   },
 })
-/*
-type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
-export const HomeScreen = withAuthRequired(
-  observer(function Home(_opts: Props) {
-    const store = useStores()
-    const onMainScroll = useOnMainScroll(store)
-    const {screen, track} = useAnalytics()
-    const scrollElRef = React.useRef<FlatList>(null)
-    const {appState} = useAppState({
-      onForeground: () => doPoll(true),
-    })
-    const isFocused = useIsFocused()
-
-    const doPoll = React.useCallback(
-      (knownActive = false) => {
-        if ((!knownActive && appState !== 'active') || !isFocused) {
-          return
-        }
-        if (store.me.mainFeed.isLoading) {
-          return
-        }
-        store.log.debug('HomeScreen: Polling for new posts')
-        store.me.mainFeed.checkForLatest()
-      },
-      [appState, isFocused, store],
-    )
-
-    const scrollToTop = React.useCallback(() => {
-      // NOTE: the feed is offset by the height of the collapsing header,
-      //       so we scroll to the negative of that height -prf
-      scrollElRef.current?.scrollToOffset({offset: -HEADER_HEIGHT})
-    }, [scrollElRef])
-
-    useFocusEffect(
-      React.useCallback(() => {
-        const softResetSub = store.onScreenSoftReset(scrollToTop)
-        const feedCleanup = store.me.mainFeed.registerListeners()
-        const pollInterval = setInterval(doPoll, 15e3)
-
-        screen('Feed')
-        store.log.debug('HomeScreen: Updating feed')
-        if (store.me.mainFeed.hasContent) {
-          store.me.mainFeed.update()
-        }
-
-        return () => {
-          clearInterval(pollInterval)
-          softResetSub.remove()
-          feedCleanup()
-        }
-      }, [store, doPoll, scrollToTop, screen]),
-    )
-
-    const onPressCompose = React.useCallback(() => {
-      track('HomeScreen:PressCompose')
-      store.shell.openComposer({})
-    }, [store, track])
-
-    const onPressTryAgain = React.useCallback(() => {
-      store.me.mainFeed.refresh()
-    }, [store])
-
-    const onPressLoadLatest = React.useCallback(() => {
-      store.me.mainFeed.refresh()
-      scrollToTop()
-    }, [store, scrollToTop])
-
-    return (
-      <View style={s.hContentRegion}>
-        {store.shell.isOnboarding && <WelcomeBanner />}
-        <Feed
-          testID="homeFeed"
-          key="default"
-          feed={store.me.mainFeed}
-          scrollElRef={scrollElRef}
-          style={s.hContentRegion}
-          showPostFollowBtn
-          onPressTryAgain={onPressTryAgain}
-          onScroll={onMainScroll}
-          headerOffset={store.shell.isOnboarding ? 0 : HEADER_HEIGHT}
-        />
-        {!store.shell.isOnboarding && (
-          <ViewHeader title="Bluesky" canGoBack={false} hideOnScroll />
-        )}
-        {store.me.mainFeed.hasNewLatest && !store.me.mainFeed.isRefreshing && (
-          <LoadLatestBtn onPress={onPressLoadLatest} />
-        )}
-        <FAB
-          testID="composeFAB"
-          onPress={onPressCompose}
-          icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
-        />
-      </View>
-    )
-  }),
-)
-*/