From 257686f3603e800e355850a23b3a4011e5558aeb Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Thu, 25 May 2023 20:02:37 -0500
Subject: Add feeds tab

---
 src/view/com/posts/MultiFeed.tsx | 230 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 230 insertions(+)
 create mode 100644 src/view/com/posts/MultiFeed.tsx

(limited to 'src/view/com/posts/MultiFeed.tsx')

diff --git a/src/view/com/posts/MultiFeed.tsx b/src/view/com/posts/MultiFeed.tsx
new file mode 100644
index 000000000..4911c9e2c
--- /dev/null
+++ b/src/view/com/posts/MultiFeed.tsx
@@ -0,0 +1,230 @@
+import React, {MutableRefObject} from 'react'
+import {observer} from 'mobx-react-lite'
+import {
+  ActivityIndicator,
+  RefreshControl,
+  StyleProp,
+  StyleSheet,
+  View,
+  ViewStyle,
+} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {FlatList} from '../util/Views'
+import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
+import {ErrorMessage} from '../util/error/ErrorMessage'
+import {PostsMultiFeedModel, MultiFeedItem} from 'state/models/feeds/multi-feed'
+import {FeedSlice} from './FeedSlice'
+import {Text} from '../util/text/Text'
+import {Link} from '../util/Link'
+import {UserAvatar} from '../util/UserAvatar'
+import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
+import {s} from 'lib/styles'
+import {useAnalytics} from 'lib/analytics'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useTheme} from 'lib/ThemeContext'
+
+export const MultiFeed = observer(function Feed({
+  multifeed,
+  style,
+  showPostFollowBtn,
+  scrollElRef,
+  onScroll,
+  scrollEventThrottle,
+  testID,
+  headerOffset = 0,
+  extraData,
+}: {
+  multifeed: PostsMultiFeedModel
+  style?: StyleProp<ViewStyle>
+  showPostFollowBtn?: boolean
+  scrollElRef?: MutableRefObject<FlatList<any> | null>
+  onPressTryAgain?: () => void
+  onScroll?: OnScrollCb
+  scrollEventThrottle?: number
+  renderEmptyState?: () => JSX.Element
+  testID?: string
+  headerOffset?: number
+  extraData?: any
+}) {
+  const pal = usePalette('default')
+  const palInverted = usePalette('inverted')
+  const theme = useTheme()
+  const {track} = useAnalytics()
+  const [isRefreshing, setIsRefreshing] = React.useState(false)
+
+  // events
+  // =
+
+  const onRefresh = React.useCallback(async () => {
+    track('MultiFeed:onRefresh')
+    setIsRefreshing(true)
+    try {
+      await multifeed.refresh()
+    } catch (err) {
+      multifeed.rootStore.log.error('Failed to refresh posts feed', err)
+    }
+    setIsRefreshing(false)
+  }, [multifeed, track, setIsRefreshing])
+
+  const onEndReached = React.useCallback(async () => {
+    track('MultiFeed:onEndReached')
+    try {
+      await multifeed.loadMore()
+    } catch (err) {
+      multifeed.rootStore.log.error('Failed to load more posts', err)
+    }
+  }, [multifeed, track])
+
+  // rendering
+  // =
+
+  const renderItem = React.useCallback(
+    ({item}: {item: MultiFeedItem}) => {
+      if (item.type === 'header') {
+        return <View style={[styles.header, pal.border]} />
+      } else if (item.type === 'feed-header') {
+        return (
+          <View style={styles.feedHeader}>
+            <UserAvatar type="algo" avatar={item.avatar} size={28} />
+            <Text type="title-lg" style={[pal.text, styles.feedHeaderTitle]}>
+              {item.title}
+            </Text>
+          </View>
+        )
+      } else if (item.type === 'feed-slice') {
+        return (
+          <FeedSlice slice={item.slice} showFollowBtn={showPostFollowBtn} />
+        )
+      } else if (item.type === 'feed-loading') {
+        return <PostFeedLoadingPlaceholder />
+      } else if (item.type === 'feed-error') {
+        return <ErrorMessage message={item.error} />
+      } else if (item.type === 'feed-footer') {
+        return (
+          <Link
+            href={item.uri}
+            style={[styles.feedFooter, pal.border, pal.view]}>
+            <Text type="lg" style={pal.link}>
+              See more from {item.title}
+            </Text>
+            <FontAwesomeIcon
+              icon="angle-right"
+              size={18}
+              color={pal.colors.link}
+            />
+          </Link>
+        )
+      } else if (item.type === 'footer') {
+        return (
+          <Link
+            style={[styles.footerLink, palInverted.view]}
+            href="/search/feeds">
+            <FontAwesomeIcon
+              icon="search"
+              size={18}
+              color={palInverted.colors.text}
+            />
+            <Text type="lg-medium" style={palInverted.text}>
+              Discover new feeds
+            </Text>
+          </Link>
+        )
+      }
+      return null
+    },
+    [showPostFollowBtn, pal, palInverted],
+  )
+
+  const FeedFooter = React.useCallback(
+    () =>
+      multifeed.isLoading && !isRefreshing ? (
+        <View style={styles.loadMore}>
+          <ActivityIndicator color={pal.colors.text} />
+        </View>
+      ) : (
+        <View />
+      ),
+    [multifeed.isLoading, isRefreshing, pal],
+  )
+
+  return (
+    <View testID={testID} style={style}>
+      {multifeed.items.length > 0 && (
+        <FlatList
+          testID={testID ? `${testID}-flatlist` : undefined}
+          ref={scrollElRef}
+          data={multifeed.items}
+          keyExtractor={item => item._reactKey}
+          renderItem={renderItem}
+          ListFooterComponent={FeedFooter}
+          refreshControl={
+            <RefreshControl
+              refreshing={isRefreshing}
+              onRefresh={onRefresh}
+              tintColor={pal.colors.text}
+              titleColor={pal.colors.text}
+              progressViewOffset={headerOffset}
+            />
+          }
+          contentContainerStyle={s.contentContainer}
+          style={[{paddingTop: headerOffset}, pal.viewLight, styles.container]}
+          onScroll={onScroll}
+          scrollEventThrottle={scrollEventThrottle}
+          indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
+          onEndReached={onEndReached}
+          onEndReachedThreshold={0.6}
+          removeClippedSubviews={true}
+          contentOffset={{x: 0, y: headerOffset * -1}}
+          extraData={extraData}
+          // @ts-ignore our .web version only -prf
+          desktopFixedHeight
+        />
+      )}
+    </View>
+  )
+})
+
+const styles = StyleSheet.create({
+  container: {
+    height: '100%',
+  },
+  header: {
+    borderTopWidth: 1,
+    marginBottom: 4,
+  },
+  feedHeader: {
+    flexDirection: 'row',
+    gap: 8,
+    alignItems: 'center',
+    paddingHorizontal: 16,
+    paddingBottom: 8,
+    marginTop: 12,
+  },
+  feedHeaderTitle: {
+    fontWeight: 'bold',
+  },
+  feedFooter: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    paddingHorizontal: 16,
+    paddingVertical: 16,
+    marginBottom: 12,
+    borderTopWidth: 1,
+    borderBottomWidth: 1,
+  },
+  footerLink: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    borderRadius: 8,
+    paddingHorizontal: 14,
+    paddingVertical: 12,
+    marginHorizontal: 8,
+    marginBottom: 8,
+    gap: 8,
+  },
+  loadMore: {
+    paddingTop: 10,
+  },
+})
-- 
cgit 1.4.1


From 651f3abc1ff0913347ed7bfb824f8e280f87a850 Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Thu, 25 May 2023 20:19:49 -0500
Subject: Tune up custom feed uis on web

---
 src/view/com/pager/TabBar.tsx      |  5 +++--
 src/view/com/posts/MultiFeed.tsx   | 29 ++++++++++++++++++++++++++++-
 src/view/shell/desktop/LeftNav.tsx | 18 +++++++++---------
 3 files changed, 40 insertions(+), 12 deletions(-)

(limited to 'src/view/com/posts/MultiFeed.tsx')

diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx
index 70500db2c..422d986a7 100644
--- a/src/view/com/pager/TabBar.tsx
+++ b/src/view/com/pager/TabBar.tsx
@@ -92,7 +92,7 @@ export function TabBar({
               hoverStyle={pal.viewLight}
               onPress={() => onPressItem(i)}>
               <Text
-                type="lg-bold"
+                type={isDesktopWeb ? 'xl-bold' : 'lg-bold'}
                 testID={testID ? `${testID}-${item}` : undefined}
                 style={selected ? pal.text : pal.textLight}>
                 {item}
@@ -112,7 +112,7 @@ const styles = isDesktopWeb
         width: 598,
       },
       contentContainer: {
-        columnGap: 14,
+        columnGap: 8,
         marginLeft: 14,
         paddingRight: 14,
         backgroundColor: 'transparent',
@@ -120,6 +120,7 @@ const styles = isDesktopWeb
       item: {
         paddingTop: 14,
         paddingBottom: 12,
+        paddingHorizontal: 10,
         borderBottomWidth: 3,
         borderBottomColor: 'transparent',
       },
diff --git a/src/view/com/posts/MultiFeed.tsx b/src/view/com/posts/MultiFeed.tsx
index 4911c9e2c..75c57c446 100644
--- a/src/view/com/posts/MultiFeed.tsx
+++ b/src/view/com/posts/MultiFeed.tsx
@@ -22,6 +22,8 @@ import {s} from 'lib/styles'
 import {useAnalytics} from 'lib/analytics'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useTheme} from 'lib/ThemeContext'
+import {isDesktopWeb} from 'platform/detection'
+import {CogIcon} from 'lib/icons'
 
 export const MultiFeed = observer(function Feed({
   multifeed,
@@ -81,6 +83,18 @@ export const MultiFeed = observer(function Feed({
   const renderItem = React.useCallback(
     ({item}: {item: MultiFeedItem}) => {
       if (item.type === 'header') {
+        if (isDesktopWeb) {
+          return (
+            <View style={[pal.view, pal.border, styles.headerDesktop]}>
+              <Text type="2xl-bold" style={pal.text}>
+                My Feeds
+              </Text>
+              <Link href="/settings/saved-feeds">
+                <CogIcon strokeWidth={1.5} style={pal.icon} size={28} />
+              </Link>
+            </View>
+          )
+        }
         return <View style={[styles.header, pal.border]} />
       } else if (item.type === 'feed-header') {
         return (
@@ -167,7 +181,11 @@ export const MultiFeed = observer(function Feed({
             />
           }
           contentContainerStyle={s.contentContainer}
-          style={[{paddingTop: headerOffset}, pal.viewLight, styles.container]}
+          style={[
+            {paddingTop: headerOffset},
+            isDesktopWeb ? pal.view : pal.viewLight,
+            styles.container,
+          ]}
           onScroll={onScroll}
           scrollEventThrottle={scrollEventThrottle}
           indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
@@ -192,6 +210,15 @@ const styles = StyleSheet.create({
     borderTopWidth: 1,
     marginBottom: 4,
   },
+  headerDesktop: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    borderBottomWidth: 1,
+    marginBottom: 4,
+    paddingHorizontal: 16,
+    paddingVertical: 8,
+  },
   feedHeader: {
     flexDirection: 'row',
     gap: 8,
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index 3b14d7e99..ce232a730 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -197,15 +197,6 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() {
         }
         label="Search"
       />
-      <NavItem
-        href="/notifications"
-        count={store.me.notifications.unreadCountLabel}
-        icon={<BellIcon strokeWidth={2} size={24} style={pal.text} />}
-        iconFilled={
-          <BellIconSolid strokeWidth={1.5} size={24} style={pal.text} />
-        }
-        label="Notifications"
-      />
       <NavItem
         href="/feeds"
         icon={
@@ -224,6 +215,15 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() {
         }
         label="My Feeds"
       />
+      <NavItem
+        href="/notifications"
+        count={store.me.notifications.unreadCountLabel}
+        icon={<BellIcon strokeWidth={2} size={24} style={pal.text} />}
+        iconFilled={
+          <BellIconSolid strokeWidth={1.5} size={24} style={pal.text} />
+        }
+        label="Notifications"
+      />
       <NavItem
         href="/moderation"
         icon={
-- 
cgit 1.4.1


From 68332bc8772462e9adf1f7640890d3ee850f2e04 Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Thu, 25 May 2023 20:44:37 -0500
Subject: UI tune ups

---
 src/view/com/pager/FeedsTabBarMobile.tsx | 10 +++++++---
 src/view/com/posts/MultiFeed.tsx         | 21 +++++----------------
 2 files changed, 12 insertions(+), 19 deletions(-)

(limited to 'src/view/com/posts/MultiFeed.tsx')

diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx
index 0d71b2b98..9c7138815 100644
--- a/src/view/com/pager/FeedsTabBarMobile.tsx
+++ b/src/view/com/pager/FeedsTabBarMobile.tsx
@@ -6,6 +6,7 @@ import {RenderTabBarFnProps} from 'view/com/pager/Pager'
 import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
+import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
 import {Link} from '../util/Link'
 import {Text} from '../util/text/Text'
 import {CogIcon} from 'lib/icons'
@@ -32,6 +33,8 @@ export const FeedsTabBar = observer(
       transform: [{translateY: Animated.multiply(interp, -100)}],
     }
 
+    const brandBlue = useColorSchemeStyle(s.brandBlue, s.blue3)
+
     const onPressAvi = React.useCallback(() => {
       store.shell.openDrawer()
     }, [store])
@@ -59,9 +62,7 @@ export const FeedsTabBar = observer(
               />
             </TouchableOpacity>
           </View>
-          <Text type="title-lg" style={[s.brandBlue, s.bold]}>
-            Bluesky
-          </Text>
+          <Text style={[brandBlue, s.bold, styles.title]}>Bluesky</Text>
           <View style={[pal.view]}>
             <Link
               href="/settings/saved-feeds"
@@ -104,4 +105,7 @@ const styles = StyleSheet.create({
     paddingBottom: 2,
     width: '100%',
   },
+  title: {
+    fontSize: 21,
+  },
 })
diff --git a/src/view/com/posts/MultiFeed.tsx b/src/view/com/posts/MultiFeed.tsx
index 75c57c446..db353909c 100644
--- a/src/view/com/posts/MultiFeed.tsx
+++ b/src/view/com/posts/MultiFeed.tsx
@@ -49,7 +49,6 @@ export const MultiFeed = observer(function Feed({
   extraData?: any
 }) {
   const pal = usePalette('default')
-  const palInverted = usePalette('inverted')
   const theme = useTheme()
   const {track} = useAnalytics()
   const [isRefreshing, setIsRefreshing] = React.useState(false)
@@ -130,15 +129,9 @@ export const MultiFeed = observer(function Feed({
         )
       } else if (item.type === 'footer') {
         return (
-          <Link
-            style={[styles.footerLink, palInverted.view]}
-            href="/search/feeds">
-            <FontAwesomeIcon
-              icon="search"
-              size={18}
-              color={palInverted.colors.text}
-            />
-            <Text type="lg-medium" style={palInverted.text}>
+          <Link style={[styles.footerLink, pal.viewLight]} href="/search/feeds">
+            <FontAwesomeIcon icon="search" size={18} color={pal.colors.text} />
+            <Text type="xl-medium" style={pal.text}>
               Discover new feeds
             </Text>
           </Link>
@@ -146,7 +139,7 @@ export const MultiFeed = observer(function Feed({
       }
       return null
     },
-    [showPostFollowBtn, pal, palInverted],
+    [showPostFollowBtn, pal],
   )
 
   const FeedFooter = React.useCallback(
@@ -181,11 +174,7 @@ export const MultiFeed = observer(function Feed({
             />
           }
           contentContainerStyle={s.contentContainer}
-          style={[
-            {paddingTop: headerOffset},
-            isDesktopWeb ? pal.view : pal.viewLight,
-            styles.container,
-          ]}
+          style={[{paddingTop: headerOffset}, pal.view, styles.container]}
           onScroll={onScroll}
           scrollEventThrottle={scrollEventThrottle}
           indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
-- 
cgit 1.4.1

