From 3d09008bfde3352212ab69105063150a175b8d5d Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Wed, 17 May 2023 14:03:08 -0500
Subject: Reorganize custom feeds in the view layer

---
 src/view/com/feeds/CustomFeed.tsx | 153 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 153 insertions(+)
 create mode 100644 src/view/com/feeds/CustomFeed.tsx

(limited to 'src/view/com/feeds/CustomFeed.tsx')

diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx
new file mode 100644
index 000000000..2137081f3
--- /dev/null
+++ b/src/view/com/feeds/CustomFeed.tsx
@@ -0,0 +1,153 @@
+import React from 'react'
+import {
+  StyleProp,
+  StyleSheet,
+  View,
+  ViewStyle,
+  TouchableOpacity,
+} from 'react-native'
+import {Text} from '../util/text/Text'
+import {usePalette} from 'lib/hooks/usePalette'
+import {colors, s} from 'lib/styles'
+import {UserAvatar} from '../util/UserAvatar'
+import {Button} from '../util/forms/Button'
+import {observer} from 'mobx-react-lite'
+import {CustomFeedModel} from 'state/models/feeds/custom-feed'
+import {useFocusEffect, useNavigation} from '@react-navigation/native'
+import {NavigationProp} from 'lib/routes/types'
+import {useStores} from 'state/index'
+import {HeartIconSolid} from 'lib/icons'
+import {pluralize} from 'lib/strings/helpers'
+import {AtUri} from '@atproto/api'
+import {isWeb} from 'platform/detection'
+
+const CustomFeed = observer(
+  ({
+    item,
+    style,
+    showBottom = true,
+    reloadOnFocus = false,
+  }: {
+    item: CustomFeedModel
+    style?: StyleProp<ViewStyle>
+    showBottom?: boolean
+    reloadOnFocus?: boolean
+  }) => {
+    const store = useStores()
+    const pal = usePalette('default')
+    const navigation = useNavigation<NavigationProp>()
+
+    // TODO: this is pretty hacky, but it works for now
+    // causes issues on web
+    useFocusEffect(() => {
+      if (reloadOnFocus && !isWeb) {
+        item.reload()
+      }
+    })
+
+    return (
+      <TouchableOpacity
+        accessibilityRole="button"
+        style={[styles.container, style]}
+        onPress={() => {
+          navigation.navigate('CustomFeed', {
+            name: item.data.creator.did,
+            rkey: new AtUri(item.data.uri).rkey,
+            displayName:
+              item.data.displayName ??
+              `${item.data.creator.displayName}'s feed`,
+          })
+        }}
+        key={item.data.uri}>
+        <View style={[styles.headerContainer]}>
+          <View style={[s.mr10]}>
+            <UserAvatar type="algo" size={36} avatar={item.data.avatar} />
+          </View>
+          <View style={[styles.headerTextContainer]}>
+            <Text style={[pal.text, s.bold]}>
+              {item.data.displayName ?? 'Feed name'}
+            </Text>
+            <Text style={[pal.textLight, styles.description]} numberOfLines={5}>
+              {item.data.description ??
+                "Explore our Feed for the latest updates and insights! Dive into a world of intriguing articles, trending news, and exciting stories that cover a wide range of topics. From technology breakthroughs to lifestyle tips, there's something here for everyone. Stay informed and get inspired with us. Join the conversation now!"}
+            </Text>
+          </View>
+        </View>
+
+        {showBottom ? (
+          <View style={styles.bottomContainer}>
+            <View style={styles.likedByContainer}>
+              {/* <View style={styles.likedByAvatars}>
+              <UserAvatar size={24} avatar={item.data.avatar} />
+              <UserAvatar size={24} avatar={item.data.avatar} />
+              <UserAvatar size={24} avatar={item.data.avatar} />
+            </View> */}
+
+              <HeartIconSolid size={16} style={[s.mr2, {color: colors.red3}]} />
+              <Text style={[pal.text, pal.textLight]}>
+                {item.data.likeCount && item.data.likeCount > 0
+                  ? `Liked by ${item.data.likeCount} ${pluralize(
+                      item.data.likeCount,
+                      'other',
+                    )}`
+                  : 'Be the first to like this'}
+              </Text>
+            </View>
+            <View>
+              <Button
+                type={item.isSaved ? 'default' : 'inverted'}
+                onPress={() => {
+                  if (item.data.viewer?.saved) {
+                    store.me.savedFeeds.unsave(item)
+                  } else {
+                    store.me.savedFeeds.save(item)
+                  }
+                }}
+                label={item.data.viewer?.saved ? 'Unsave' : 'Save'}
+              />
+            </View>
+          </View>
+        ) : null}
+      </TouchableOpacity>
+    )
+  },
+)
+export default CustomFeed
+
+const styles = StyleSheet.create({
+  container: {
+    paddingHorizontal: 18,
+    paddingVertical: 20,
+    flexDirection: 'column',
+    flex: 1,
+    borderTopWidth: 1,
+    borderTopColor: '#E5E5E5',
+    gap: 18,
+  },
+  headerContainer: {
+    flexDirection: 'row',
+  },
+  headerTextContainer: {
+    flexDirection: 'column',
+    columnGap: 4,
+    flex: 1,
+  },
+  description: {
+    flex: 1,
+    flexWrap: 'wrap',
+  },
+  bottomContainer: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+  },
+  likedByContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 2,
+  },
+  likedByAvatars: {
+    flexDirection: 'row',
+    gap: -12,
+  },
+})
-- 
cgit 1.4.1


From a4c6dd6b1ead07e5606146408e3d78a24dc5977a Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Wed, 17 May 2023 14:24:27 -0500
Subject: Tune up the custom feed card rendering

---
 src/view/com/feeds/CustomFeed.tsx       | 71 ++++++++++++++-------------------
 src/view/com/feeds/SavedFeedItem.tsx    |  9 +----
 src/view/com/util/post-embeds/index.tsx | 14 +++++--
 src/view/screens/Profile.tsx            |  4 +-
 4 files changed, 44 insertions(+), 54 deletions(-)

(limited to 'src/view/com/feeds/CustomFeed.tsx')

diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx
index 2137081f3..edadada9c 100644
--- a/src/view/com/feeds/CustomFeed.tsx
+++ b/src/view/com/feeds/CustomFeed.tsx
@@ -16,21 +16,21 @@ import {CustomFeedModel} from 'state/models/feeds/custom-feed'
 import {useFocusEffect, useNavigation} from '@react-navigation/native'
 import {NavigationProp} from 'lib/routes/types'
 import {useStores} from 'state/index'
-import {HeartIconSolid} from 'lib/icons'
+import {HeartIcon, HeartIconSolid} from 'lib/icons'
 import {pluralize} from 'lib/strings/helpers'
 import {AtUri} from '@atproto/api'
 import {isWeb} from 'platform/detection'
 
-const CustomFeed = observer(
+export const CustomFeed = observer(
   ({
     item,
     style,
-    showBottom = true,
+    showLikes = false,
     reloadOnFocus = false,
   }: {
     item: CustomFeedModel
     style?: StyleProp<ViewStyle>
-    showBottom?: boolean
+    showLikes?: boolean
     reloadOnFocus?: boolean
   }) => {
     const store = useStores()
@@ -67,52 +67,43 @@ const CustomFeed = observer(
             <Text style={[pal.text, s.bold]}>
               {item.data.displayName ?? 'Feed name'}
             </Text>
-            <Text style={[pal.textLight, styles.description]} numberOfLines={5}>
-              {item.data.description ??
-                "Explore our Feed for the latest updates and insights! Dive into a world of intriguing articles, trending news, and exciting stories that cover a wide range of topics. From technology breakthroughs to lifestyle tips, there's something here for everyone. Stay informed and get inspired with us. Join the conversation now!"}
-            </Text>
+            <Text style={[pal.textLight]}>by @{item.data.creator.handle}</Text>
+          </View>
+          <View>
+            <Button
+              type={item.isSaved ? 'default' : 'inverted'}
+              onPress={() => {
+                if (item.data.viewer?.saved) {
+                  store.me.savedFeeds.unsave(item)
+                } else {
+                  store.me.savedFeeds.save(item)
+                }
+              }}
+              label={item.data.viewer?.saved ? 'Unsave' : 'Save'}
+            />
           </View>
         </View>
 
-        {showBottom ? (
+        {item.data.description ? (
+          <Text style={[pal.textLight, styles.description]} numberOfLines={3}>
+            {item.data.description}
+          </Text>
+        ) : null}
+
+        {showLikes ? (
           <View style={styles.bottomContainer}>
             <View style={styles.likedByContainer}>
-              {/* <View style={styles.likedByAvatars}>
-              <UserAvatar size={24} avatar={item.data.avatar} />
-              <UserAvatar size={24} avatar={item.data.avatar} />
-              <UserAvatar size={24} avatar={item.data.avatar} />
-            </View> */}
-
-              <HeartIconSolid size={16} style={[s.mr2, {color: colors.red3}]} />
-              <Text style={[pal.text, pal.textLight]}>
-                {item.data.likeCount && item.data.likeCount > 0
-                  ? `Liked by ${item.data.likeCount} ${pluralize(
-                      item.data.likeCount,
-                      'other',
-                    )}`
-                  : 'Be the first to like this'}
+              <Text type="sm-medium" style={[pal.text, pal.textLight]}>
+                Liked by {item.data.likeCount || 0}{' '}
+                {pluralize(item.data.likeCount || 0, 'user')}
               </Text>
             </View>
-            <View>
-              <Button
-                type={item.isSaved ? 'default' : 'inverted'}
-                onPress={() => {
-                  if (item.data.viewer?.saved) {
-                    store.me.savedFeeds.unsave(item)
-                  } else {
-                    store.me.savedFeeds.save(item)
-                  }
-                }}
-                label={item.data.viewer?.saved ? 'Unsave' : 'Save'}
-              />
-            </View>
           </View>
         ) : null}
       </TouchableOpacity>
     )
   },
 )
-export default CustomFeed
 
 const styles = StyleSheet.create({
   container: {
@@ -122,7 +113,7 @@ const styles = StyleSheet.create({
     flex: 1,
     borderTopWidth: 1,
     borderTopColor: '#E5E5E5',
-    gap: 18,
+    gap: 14,
   },
   headerContainer: {
     flexDirection: 'row',
@@ -146,8 +137,4 @@ const styles = StyleSheet.create({
     alignItems: 'center',
     gap: 2,
   },
-  likedByAvatars: {
-    flexDirection: 'row',
-    gap: -12,
-  },
 })
diff --git a/src/view/com/feeds/SavedFeedItem.tsx b/src/view/com/feeds/SavedFeedItem.tsx
index b93cebe1e..9ebededc4 100644
--- a/src/view/com/feeds/SavedFeedItem.tsx
+++ b/src/view/com/feeds/SavedFeedItem.tsx
@@ -5,7 +5,7 @@ import {colors} from 'lib/styles'
 import {observer} from 'mobx-react-lite'
 import {CustomFeedModel} from 'state/models/feeds/custom-feed'
 import {SavedFeedsModel} from 'state/models/ui/saved-feeds'
-import CustomFeed from './CustomFeed'
+import {CustomFeed} from './CustomFeed'
 
 export const SavedFeedItem = observer(
   ({
@@ -19,12 +19,7 @@ export const SavedFeedItem = observer(
 
     return (
       <View style={styles.itemContainer}>
-        <CustomFeed
-          key={item.data.uri}
-          item={item}
-          showBottom={false}
-          style={styles.item}
-        />
+        <CustomFeed key={item.data.uri} item={item} style={styles.item} />
         <TouchableOpacity
           accessibilityRole="button"
           onPress={() => {
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index 1f8e48810..ed2f8feba 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -25,7 +25,7 @@ import {ExternalLinkEmbed} from './ExternalLinkEmbed'
 import {getYoutubeVideoId} from 'lib/strings/url-helpers'
 import QuoteEmbed from './QuoteEmbed'
 import {AutoSizedImage} from '../images/AutoSizedImage'
-import CustomFeed from 'view/com/feeds/CustomFeed'
+import {CustomFeed} from 'view/com/feeds/CustomFeed'
 import {CustomFeedModel} from 'state/models/feeds/custom-feed'
 
 type Embed =
@@ -173,8 +173,9 @@ export function PostEmbeds({
     return (
       <CustomFeed
         item={new CustomFeedModel(store, embed.record)}
-        style={[pal.view, pal.border, styles.extOuter]}
-        reloadOnFocus={true}
+        style={[pal.view, pal.border, styles.customFeedOuter]}
+        reloadOnFocus
+        showLikes
       />
     )
   }
@@ -198,6 +199,13 @@ const styles = StyleSheet.create({
     borderRadius: 8,
     marginTop: 4,
   },
+  customFeedOuter: {
+    borderWidth: 1,
+    borderRadius: 8,
+    marginTop: 4,
+    paddingHorizontal: 12,
+    paddingVertical: 12,
+  },
   alt: {
     backgroundColor: 'rgba(0, 0, 0, 0.75)',
     borderRadius: 6,
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index a80e36237..9224a5967 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -25,7 +25,7 @@ import {FAB} from '../com/util/fab/FAB'
 import {s, colors} from 'lib/styles'
 import {useAnalytics} from 'lib/analytics'
 import {ComposeIcon2} from 'lib/icons'
-import CustomFeed from 'view/com/feeds/CustomFeed'
+import {CustomFeed} from 'view/com/feeds/CustomFeed'
 import {CustomFeedModel} from 'state/models/feeds/custom-feed'
 import {useSetTitle} from 'lib/hooks/useSetTitle'
 import {combinedDisplayName} from 'lib/strings/display-names'
@@ -189,7 +189,7 @@ export const ProfileScreen = withAuthRequired(
               <FeedSlice slice={item} ignoreMuteFor={uiState.profile.did} />
             )
           } else if (item instanceof CustomFeedModel) {
-            return <CustomFeed item={item} />
+            return <CustomFeed item={item} showLikes />
           }
         }
         return <View />
-- 
cgit 1.4.1


From fb03bbf288013596cbb4c1e866aea62721ba2bcd Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Wed, 17 May 2023 14:26:23 -0500
Subject: Fix some palette issues

---
 src/view/com/feeds/CustomFeed.tsx | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

(limited to 'src/view/com/feeds/CustomFeed.tsx')

diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx
index edadada9c..cc9b66086 100644
--- a/src/view/com/feeds/CustomFeed.tsx
+++ b/src/view/com/feeds/CustomFeed.tsx
@@ -8,7 +8,7 @@ import {
 } from 'react-native'
 import {Text} from '../util/text/Text'
 import {usePalette} from 'lib/hooks/usePalette'
-import {colors, s} from 'lib/styles'
+import {s} from 'lib/styles'
 import {UserAvatar} from '../util/UserAvatar'
 import {Button} from '../util/forms/Button'
 import {observer} from 'mobx-react-lite'
@@ -16,7 +16,6 @@ import {CustomFeedModel} from 'state/models/feeds/custom-feed'
 import {useFocusEffect, useNavigation} from '@react-navigation/native'
 import {NavigationProp} from 'lib/routes/types'
 import {useStores} from 'state/index'
-import {HeartIcon, HeartIconSolid} from 'lib/icons'
 import {pluralize} from 'lib/strings/helpers'
 import {AtUri} from '@atproto/api'
 import {isWeb} from 'platform/detection'
@@ -48,7 +47,7 @@ export const CustomFeed = observer(
     return (
       <TouchableOpacity
         accessibilityRole="button"
-        style={[styles.container, style]}
+        style={[styles.container, pal.border, style]}
         onPress={() => {
           navigation.navigate('CustomFeed', {
             name: item.data.creator.did,
@@ -112,7 +111,6 @@ const styles = StyleSheet.create({
     flexDirection: 'column',
     flex: 1,
     borderTopWidth: 1,
-    borderTopColor: '#E5E5E5',
     gap: 14,
   },
   headerContainer: {
-- 
cgit 1.4.1


From 4e56d24a9d72da4e7c3ddac6ae85b3913f2b365b Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Wed, 17 May 2023 14:34:37 -0500
Subject: Dont show save button in embed cards

---
 src/view/com/feeds/CustomFeed.tsx    | 31 +++++++++++++++++--------------
 src/view/com/feeds/SavedFeedItem.tsx |  7 ++++++-
 src/view/screens/Profile.tsx         |  2 +-
 3 files changed, 24 insertions(+), 16 deletions(-)

(limited to 'src/view/com/feeds/CustomFeed.tsx')

diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx
index cc9b66086..e4e0d50c2 100644
--- a/src/view/com/feeds/CustomFeed.tsx
+++ b/src/view/com/feeds/CustomFeed.tsx
@@ -24,11 +24,13 @@ export const CustomFeed = observer(
   ({
     item,
     style,
+    showSaveBtn = false,
     showLikes = false,
     reloadOnFocus = false,
   }: {
     item: CustomFeedModel
     style?: StyleProp<ViewStyle>
+    showSaveBtn?: boolean
     showLikes?: boolean
     reloadOnFocus?: boolean
   }) => {
@@ -66,21 +68,22 @@ export const CustomFeed = observer(
             <Text style={[pal.text, s.bold]}>
               {item.data.displayName ?? 'Feed name'}
             </Text>
-            <Text style={[pal.textLight]}>by @{item.data.creator.handle}</Text>
-          </View>
-          <View>
-            <Button
-              type={item.isSaved ? 'default' : 'inverted'}
-              onPress={() => {
-                if (item.data.viewer?.saved) {
-                  store.me.savedFeeds.unsave(item)
-                } else {
-                  store.me.savedFeeds.save(item)
-                }
-              }}
-              label={item.data.viewer?.saved ? 'Unsave' : 'Save'}
-            />
           </View>
+          {showSaveBtn && (
+            <View>
+              <Button
+                type={item.isSaved ? 'default' : 'inverted'}
+                onPress={() => {
+                  if (item.data.viewer?.saved) {
+                    store.me.savedFeeds.unsave(item)
+                  } else {
+                    store.me.savedFeeds.save(item)
+                  }
+                }}
+                label={item.data.viewer?.saved ? 'Unsave' : 'Save'}
+              />
+            </View>
+          )}
         </View>
 
         {item.data.description ? (
diff --git a/src/view/com/feeds/SavedFeedItem.tsx b/src/view/com/feeds/SavedFeedItem.tsx
index 9ebededc4..329f1811e 100644
--- a/src/view/com/feeds/SavedFeedItem.tsx
+++ b/src/view/com/feeds/SavedFeedItem.tsx
@@ -19,7 +19,12 @@ export const SavedFeedItem = observer(
 
     return (
       <View style={styles.itemContainer}>
-        <CustomFeed key={item.data.uri} item={item} style={styles.item} />
+        <CustomFeed
+          key={item.data.uri}
+          item={item}
+          style={styles.item}
+          showSaveBtn
+        />
         <TouchableOpacity
           accessibilityRole="button"
           onPress={() => {
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 9224a5967..bf312cd06 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -189,7 +189,7 @@ export const ProfileScreen = withAuthRequired(
               <FeedSlice slice={item} ignoreMuteFor={uiState.profile.did} />
             )
           } else if (item instanceof CustomFeedModel) {
-            return <CustomFeed item={item} showLikes />
+            return <CustomFeed item={item} showSaveBtn showLikes />
           }
         }
         return <View />
-- 
cgit 1.4.1


From 9c02fbb925290b9f147e1622f4f41f5a956dce5c Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Wed, 17 May 2023 14:38:23 -0500
Subject: Unify the display name fallback behavior

---
 src/state/models/feeds/custom-feed.ts |  8 ++++++++
 src/state/models/ui/saved-feeds.ts    |  8 ++------
 src/view/com/feeds/CustomFeed.tsx     | 11 ++++++-----
 3 files changed, 16 insertions(+), 11 deletions(-)

(limited to 'src/view/com/feeds/CustomFeed.tsx')

diff --git a/src/state/models/feeds/custom-feed.ts b/src/state/models/feeds/custom-feed.ts
index 20eef49ad..e60ac2740 100644
--- a/src/state/models/feeds/custom-feed.ts
+++ b/src/state/models/feeds/custom-feed.ts
@@ -1,6 +1,7 @@
 import {AppBskyFeedDefs, AtUri} from '@atproto/api'
 import {makeAutoObservable} from 'mobx'
 import {RootStoreModel} from 'state/models/root-store'
+import {sanitizeDisplayName} from 'lib/strings/display-names'
 
 export class CustomFeedModel {
   // data
@@ -34,6 +35,13 @@ export class CustomFeedModel {
     return this.data.uri
   }
 
+  get displayName() {
+    if (this.data.displayName) {
+      return sanitizeDisplayName(this.data.displayName)
+    }
+    return `Feed by @${this.data.creator.handle}`
+  }
+
   get isSaved() {
     return this.data.viewer?.saved
   }
diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts
index ce0de25ec..85e77cae5 100644
--- a/src/state/models/ui/saved-feeds.ts
+++ b/src/state/models/ui/saved-feeds.ts
@@ -66,15 +66,11 @@ export class SavedFeedsModel {
   }
 
   get listOfFeedNames() {
-    return this.feeds.map(
-      f => f.data.displayName ?? f.data.creator.displayName + "'s feed",
-    )
+    return this.feeds.map(f => f.displayName)
   }
 
   get listOfPinnedFeedNames() {
-    return this.pinned.map(
-      f => f.data.displayName ?? f.data.creator.displayName + "'s feed",
-    )
+    return this.pinned.map(f => f.displayName)
   }
 
   get savedFeedsWithoutPinned() {
diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx
index e4e0d50c2..95726be64 100644
--- a/src/view/com/feeds/CustomFeed.tsx
+++ b/src/view/com/feeds/CustomFeed.tsx
@@ -54,9 +54,7 @@ export const CustomFeed = observer(
           navigation.navigate('CustomFeed', {
             name: item.data.creator.did,
             rkey: new AtUri(item.data.uri).rkey,
-            displayName:
-              item.data.displayName ??
-              `${item.data.creator.displayName}'s feed`,
+            displayName: item.displayName,
           })
         }}
         key={item.data.uri}>
@@ -65,8 +63,11 @@ export const CustomFeed = observer(
             <UserAvatar type="algo" size={36} avatar={item.data.avatar} />
           </View>
           <View style={[styles.headerTextContainer]}>
-            <Text style={[pal.text, s.bold]}>
-              {item.data.displayName ?? 'Feed name'}
+            <Text style={[pal.text, s.bold]} numberOfLines={3}>
+              {item.displayName}
+            </Text>
+            <Text style={[pal.textLight]} numberOfLines={3}>
+              by @{item.data.creator.handle}
             </Text>
           </View>
           {showSaveBtn && (
-- 
cgit 1.4.1


From 0f5b3808f8107ff4f04cda8c3da18ffcdcf6f27f Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Wed, 17 May 2023 15:03:19 -0500
Subject: Remove a focus-effect that was triggering a rerender loop

---
 src/view/com/feeds/CustomFeed.tsx       | 13 +------------
 src/view/com/util/post-embeds/index.tsx | 26 +++++++++++++++++---------
 2 files changed, 18 insertions(+), 21 deletions(-)

(limited to 'src/view/com/feeds/CustomFeed.tsx')

diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx
index 95726be64..8e1a78453 100644
--- a/src/view/com/feeds/CustomFeed.tsx
+++ b/src/view/com/feeds/CustomFeed.tsx
@@ -13,12 +13,11 @@ import {UserAvatar} from '../util/UserAvatar'
 import {Button} from '../util/forms/Button'
 import {observer} from 'mobx-react-lite'
 import {CustomFeedModel} from 'state/models/feeds/custom-feed'
-import {useFocusEffect, useNavigation} from '@react-navigation/native'
+import {useNavigation} from '@react-navigation/native'
 import {NavigationProp} from 'lib/routes/types'
 import {useStores} from 'state/index'
 import {pluralize} from 'lib/strings/helpers'
 import {AtUri} from '@atproto/api'
-import {isWeb} from 'platform/detection'
 
 export const CustomFeed = observer(
   ({
@@ -26,26 +25,16 @@ export const CustomFeed = observer(
     style,
     showSaveBtn = false,
     showLikes = false,
-    reloadOnFocus = false,
   }: {
     item: CustomFeedModel
     style?: StyleProp<ViewStyle>
     showSaveBtn?: boolean
     showLikes?: boolean
-    reloadOnFocus?: boolean
   }) => {
     const store = useStores()
     const pal = usePalette('default')
     const navigation = useNavigation<NavigationProp>()
 
-    // TODO: this is pretty hacky, but it works for now
-    // causes issues on web
-    useFocusEffect(() => {
-      if (reloadOnFocus && !isWeb) {
-        item.reload()
-      }
-    })
-
     return (
       <TouchableOpacity
         accessibilityRole="button"
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index 01d26db91..9eb140c2e 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -170,20 +170,28 @@ export function PostEmbeds({
     AppBskyEmbedRecord.isView(embed) &&
     AppBskyFeedDefs.isGeneratorView(embed.record)
   ) {
-    // TODO memoize this?
-    return (
-      <CustomFeed
-        item={new CustomFeedModel(store, embed.record)}
-        style={[pal.view, pal.border, styles.customFeedOuter]}
-        reloadOnFocus
-        showLikes
-      />
-    )
+    return <CustomFeedEmbed record={embed.record} />
   }
 
   return <View />
 }
 
+function CustomFeedEmbed({record}: {record: AppBskyFeedDefs.GeneratorView}) {
+  const pal = usePalette('default')
+  const store = useStores()
+  const item = React.useMemo(
+    () => new CustomFeedModel(store, record),
+    [store, record],
+  )
+  return (
+    <CustomFeed
+      item={item}
+      style={[pal.view, pal.border, styles.customFeedOuter]}
+      showLikes
+    />
+  )
+}
+
 const styles = StyleSheet.create({
   stackContainer: {
     gap: 6,
-- 
cgit 1.4.1


From c55ce6de020d6c9df86c71158251caddf12da777 Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Wed, 17 May 2023 15:52:11 -0500
Subject: Rework the UI for the custom feed view

---
 src/Navigation.tsx                     |   4 +-
 src/lib/hooks/useCustomFeed.ts         |   4 +-
 src/lib/routes/types.ts                |   2 +-
 src/view/com/feeds/CustomFeed.tsx      |   3 +-
 src/view/screens/CustomFeed.tsx        | 164 -------------------
 src/view/screens/ProfileCustomFeed.tsx | 291 +++++++++++++++++++++++++++++++++
 src/view/screens/ProfileList.tsx       |   4 +-
 src/view/screens/SavedFeeds.tsx        |   5 +-
 8 files changed, 300 insertions(+), 177 deletions(-)
 delete mode 100644 src/view/screens/CustomFeed.tsx
 create mode 100644 src/view/screens/ProfileCustomFeed.tsx

(limited to 'src/view/com/feeds/CustomFeed.tsx')

diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index 4521068f2..025020afa 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -40,6 +40,7 @@ import {SettingsScreen} from './view/screens/Settings'
 import {ProfileScreen} from './view/screens/Profile'
 import {ProfileFollowersScreen} from './view/screens/ProfileFollowers'
 import {ProfileFollowsScreen} from './view/screens/ProfileFollows'
+import {ProfileCustomFeed} from './view/screens/ProfileCustomFeed'
 import {ProfileListScreen} from './view/screens/ProfileList'
 import {PostThreadScreen} from './view/screens/PostThread'
 import {PostLikedByScreen} from './view/screens/PostLikedBy'
@@ -56,7 +57,6 @@ import {ModerationMutedAccounts} from 'view/screens/ModerationMutedAccounts'
 import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts'
 import {getRoutingInstrumentation} from 'lib/sentry'
 import {SavedFeeds} from './view/screens/SavedFeeds'
-import {CustomFeed} from './view/screens/CustomFeed'
 import {PinnedFeeds} from 'view/screens/PinnedFeeds'
 import {bskyTitle} from 'lib/strings/headings'
 
@@ -127,6 +127,7 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
           title: title(`People followed by @${route.params.name}`),
         })}
       />
+      <Stack.Screen name="ProfileCustomFeed" component={ProfileCustomFeed} />
       <Stack.Screen
         name="ProfileList"
         component={ProfileListScreen}
@@ -189,7 +190,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
       />
       <Stack.Screen name="SavedFeeds" component={SavedFeeds} />
       <Stack.Screen name="PinnedFeeds" component={PinnedFeeds} />
-      <Stack.Screen name="CustomFeed" component={CustomFeed} />
     </>
   )
 }
diff --git a/src/lib/hooks/useCustomFeed.ts b/src/lib/hooks/useCustomFeed.ts
index ee40cf49e..d7a27050d 100644
--- a/src/lib/hooks/useCustomFeed.ts
+++ b/src/lib/hooks/useCustomFeed.ts
@@ -2,9 +2,9 @@ import {useEffect, useState} from 'react'
 import {useStores} from 'state/index'
 import {CustomFeedModel} from 'state/models/feeds/custom-feed'
 
-export function useCustomFeed(uri: string) {
+export function useCustomFeed(uri: string): CustomFeedModel | undefined {
   const store = useStores()
-  const [item, setItem] = useState<CustomFeedModel>()
+  const [item, setItem] = useState<CustomFeedModel | undefined>()
   useEffect(() => {
     async function fetchView() {
       const res = await store.agent.app.bsky.feed.getFeedGenerator({
diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts
index 8b96aaad7..52d0e9af2 100644
--- a/src/lib/routes/types.ts
+++ b/src/lib/routes/types.ts
@@ -13,11 +13,11 @@ export type CommonNavigatorParams = {
   Profile: {name: string; hideBackButton?: boolean}
   ProfileFollowers: {name: string}
   ProfileFollows: {name: string}
+  ProfileCustomFeed: {name: string; rkey: string}
   ProfileList: {name: string; rkey: string}
   PostThread: {name: string; rkey: string}
   PostLikedBy: {name: string; rkey: string}
   PostRepostedBy: {name: string; rkey: string}
-  CustomFeed: {name: string; rkey: string; displayName?: string}
   Debug: undefined
   Log: undefined
   Support: undefined
diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx
index 8e1a78453..5a93020a0 100644
--- a/src/view/com/feeds/CustomFeed.tsx
+++ b/src/view/com/feeds/CustomFeed.tsx
@@ -40,10 +40,9 @@ export const CustomFeed = observer(
         accessibilityRole="button"
         style={[styles.container, pal.border, style]}
         onPress={() => {
-          navigation.navigate('CustomFeed', {
+          navigation.navigate('ProfileCustomFeed', {
             name: item.data.creator.did,
             rkey: new AtUri(item.data.uri).rkey,
-            displayName: item.displayName,
           })
         }}
         key={item.data.uri}>
diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx
deleted file mode 100644
index 76125fa5c..000000000
--- a/src/view/screens/CustomFeed.tsx
+++ /dev/null
@@ -1,164 +0,0 @@
-import {NativeStackScreenProps} from '@react-navigation/native-stack'
-import {usePalette} from 'lib/hooks/usePalette'
-import {HeartIcon, HeartIconSolid} from 'lib/icons'
-import {CommonNavigatorParams} from 'lib/routes/types'
-import {makeRecordUri} from 'lib/strings/url-helpers'
-import {colors, s} from 'lib/styles'
-import {observer} from 'mobx-react-lite'
-import React, {useMemo, useRef} from 'react'
-import {FlatList, StyleSheet, TouchableOpacity, View} from 'react-native'
-import {useStores} from 'state/index'
-import {PostsFeedModel} from 'state/models/feeds/posts'
-import {useCustomFeed} from 'lib/hooks/useCustomFeed'
-import {withAuthRequired} from 'view/com/auth/withAuthRequired'
-import {Feed} from 'view/com/posts/Feed'
-import {Link} from 'view/com/util/Link'
-import {UserAvatar} from 'view/com/util/UserAvatar'
-import {ViewHeader} from 'view/com/util/ViewHeader'
-import {Button} from 'view/com/util/forms/Button'
-import {Text} from 'view/com/util/text/Text'
-
-type Props = NativeStackScreenProps<CommonNavigatorParams, 'CustomFeed'>
-export const CustomFeed = withAuthRequired(
-  observer(({route}: Props) => {
-    const rootStore = useStores()
-    const {rkey, name, displayName} = route.params
-    const uri = useMemo(
-      () => makeRecordUri(name, 'app.bsky.feed.generator', rkey),
-      [rkey, name],
-    )
-    const currentFeed = useCustomFeed(uri)
-    const scrollElRef = useRef<FlatList>(null)
-    const algoFeed: PostsFeedModel = useMemo(() => {
-      const feed = new PostsFeedModel(rootStore, 'custom', {
-        feed: uri,
-      })
-      feed.setup()
-      return feed
-    }, [rootStore, uri])
-
-    return (
-      <View style={[styles.container]}>
-        <ViewHeader
-          title={
-            displayName ?? `${currentFeed?.data.creator.displayName}'s feed`
-          }
-          showOnDesktop
-        />
-        <Feed
-          scrollElRef={scrollElRef}
-          testID={'test-feed'}
-          key="default"
-          feed={algoFeed}
-          headerOffset={12}
-          ListHeaderComponent={() => <ListHeaderComponent uri={uri} />}
-          extraData={uri}
-        />
-      </View>
-    )
-  }),
-)
-
-const ListHeaderComponent = observer(({uri}: {uri: string}) => {
-  const currentFeed = useCustomFeed(uri)
-  const pal = usePalette('default')
-  const rootStore = useStores()
-  return (
-    <View style={[styles.headerContainer]}>
-      <View style={[styles.header]}>
-        <View style={styles.avatarContainer}>
-          <UserAvatar
-            type="algo"
-            size={28}
-            avatar={currentFeed?.data.creator.avatar}
-          />
-          <Link href={`/profile/${currentFeed?.data.creator.handle}`}>
-            <Text style={[pal.textLight]}>
-              @{currentFeed?.data.creator.handle}
-            </Text>
-          </Link>
-        </View>
-        <Text style={[pal.text]}>{currentFeed?.data.description}</Text>
-      </View>
-
-      <View style={[styles.buttonsContainer]}>
-        <Button
-          type={currentFeed?.isSaved ? 'default' : 'inverted'}
-          style={[styles.saveButton]}
-          onPress={() => {
-            if (currentFeed?.data.viewer?.saved) {
-              rootStore.me.savedFeeds.unsave(currentFeed!)
-            } else {
-              rootStore.me.savedFeeds.save(currentFeed!)
-            }
-          }}
-          label={currentFeed?.data.viewer?.saved ? 'Unsave' : 'Save'}
-        />
-
-        <TouchableOpacity
-          accessibilityRole="button"
-          onPress={() => {
-            if (currentFeed?.isLiked) {
-              currentFeed?.unlike()
-            } else {
-              currentFeed?.like()
-            }
-          }}
-          style={[styles.likeButton, pal.viewLight]}>
-          <Text style={[pal.text, s.semiBold]}>
-            {currentFeed?.data.likeCount}
-          </Text>
-          {currentFeed?.isLiked ? (
-            <HeartIconSolid size={18} style={styles.liked} />
-          ) : (
-            <HeartIcon strokeWidth={3} size={18} style={styles.liked} />
-          )}
-        </TouchableOpacity>
-      </View>
-    </View>
-  )
-})
-
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-  },
-  headerContainer: {
-    alignItems: 'center',
-    justifyContent: 'center',
-    gap: 8,
-    marginBottom: 12,
-  },
-  header: {
-    alignItems: 'center',
-    gap: 4,
-  },
-  avatarContainer: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    gap: 8,
-  },
-  buttonsContainer: {
-    flexDirection: 'row',
-    gap: 8,
-  },
-  saveButton: {
-    minWidth: 100,
-    alignItems: 'center',
-  },
-  liked: {
-    color: colors.red3,
-  },
-  notLiked: {
-    color: colors.gray3,
-  },
-  likeButton: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'center',
-    paddingVertical: 4,
-    paddingHorizontal: 8,
-    borderRadius: 24,
-    gap: 4,
-  },
-})
diff --git a/src/view/screens/ProfileCustomFeed.tsx b/src/view/screens/ProfileCustomFeed.tsx
new file mode 100644
index 000000000..1113ebf01
--- /dev/null
+++ b/src/view/screens/ProfileCustomFeed.tsx
@@ -0,0 +1,291 @@
+import React, {useMemo, useRef} from 'react'
+import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {usePalette} from 'lib/hooks/usePalette'
+import {HeartIcon, HeartIconSolid} from 'lib/icons'
+import {CommonNavigatorParams} from 'lib/routes/types'
+import {makeRecordUri} from 'lib/strings/url-helpers'
+import {colors, s} from 'lib/styles'
+import {observer} from 'mobx-react-lite'
+import {FlatList, StyleSheet, View} from 'react-native'
+import {useStores} from 'state/index'
+import {PostsFeedModel} from 'state/models/feeds/posts'
+import {useCustomFeed} from 'lib/hooks/useCustomFeed'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
+import {Feed} from 'view/com/posts/Feed'
+import {pluralize} from 'lib/strings/helpers'
+import {TextLink} from 'view/com/util/Link'
+import {UserAvatar} from 'view/com/util/UserAvatar'
+import {ViewHeader} from 'view/com/util/ViewHeader'
+import {Button} from 'view/com/util/forms/Button'
+import {Text} from 'view/com/util/text/Text'
+import * as Toast from 'view/com/util/Toast'
+import {isDesktopWeb} from 'platform/detection'
+
+type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileCustomFeed'>
+export const ProfileCustomFeed = withAuthRequired(
+  observer(({route}: Props) => {
+    const store = useStores()
+    const pal = usePalette('default')
+    const {rkey, name} = route.params
+    const uri = useMemo(
+      () => makeRecordUri(name, 'app.bsky.feed.generator', rkey),
+      [rkey, name],
+    )
+    const scrollElRef = useRef<FlatList>(null)
+    const currentFeed = useCustomFeed(uri)
+    const algoFeed: PostsFeedModel = useMemo(() => {
+      const feed = new PostsFeedModel(store, 'custom', {
+        feed: uri,
+      })
+      feed.setup()
+      return feed
+    }, [store, uri])
+
+    const onToggleSaved = React.useCallback(async () => {
+      try {
+        if (currentFeed.isSaved) {
+          await currentFeed.unsave()
+        } else {
+          await currentFeed.save()
+        }
+      } catch (err) {
+        Toast.show(
+          'There was an an issue updating your feeds, please check your internet connection and try again.',
+        )
+        store.log.error('Failed up update feeds', {err})
+      }
+    }, [store, currentFeed])
+
+    const onToggleLiked = React.useCallback(async () => {
+      try {
+        if (currentFeed.isLiked) {
+          await currentFeed.unlike()
+        } else {
+          await currentFeed.like()
+        }
+      } catch (err) {
+        Toast.show(
+          'There was an an issue contacting the server, please check your internet connection and try again.',
+        )
+        store.log.error('Failed up toggle like', {err})
+      }
+    }, [store, currentFeed])
+
+    const renderHeaderBtns = React.useCallback(() => {
+      return (
+        <View style={styles.headerBtns}>
+          <Button
+            type="default"
+            testID="toggleLikeBtn"
+            accessibilityLabel="Like this feed"
+            accessibilityHint=""
+            onPress={onToggleLiked}>
+            {currentFeed?.isLiked ? (
+              <HeartIconSolid size={18} style={styles.liked} />
+            ) : (
+              <HeartIcon strokeWidth={3} size={18} style={pal.textLight} />
+            )}
+          </Button>
+          <Button
+            type={currentFeed?.isSaved ? 'default' : 'inverted'}
+            onPress={onToggleSaved}
+            accessibilityLabel={
+              currentFeed?.isSaved ? 'Unsave this feed' : 'Save this feed'
+            }
+            accessibilityHint=""
+            label={currentFeed?.isSaved ? 'Unsave' : 'Save'}
+          />
+        </View>
+      )
+    }, [
+      pal,
+      currentFeed?.isSaved,
+      currentFeed?.isLiked,
+      onToggleSaved,
+      onToggleLiked,
+    ])
+
+    const renderListHeaderComponent = React.useCallback(() => {
+      return (
+        <>
+          <View style={[styles.header, pal.border]}>
+            <View style={s.flex1}>
+              <Text
+                testID="feedName"
+                type="title-xl"
+                style={[pal.text, s.bold]}>
+                {currentFeed?.displayName}
+              </Text>
+              {currentFeed && (
+                <Text type="md" style={[pal.textLight]} numberOfLines={1}>
+                  by{' '}
+                  {currentFeed.data.creator.did === store.me.did ? (
+                    'you'
+                  ) : (
+                    <TextLink
+                      text={`@${currentFeed.data.creator.handle}`}
+                      href={`/profile/${currentFeed.data.creator.did}`}
+                    />
+                  )}
+                </Text>
+              )}
+            </View>
+            <View>
+              <UserAvatar
+                type="algo"
+                avatar={currentFeed?.data.avatar}
+                size={64}
+              />
+            </View>
+          </View>
+          <View style={styles.headerDetails}>
+            {currentFeed?.data.description ? (
+              <Text style={[pal.text, s.mb10]} numberOfLines={6}>
+                {currentFeed.data.description}
+              </Text>
+            ) : null}
+            <Text type="md-medium" style={pal.textLight}>
+              Liked by {currentFeed?.data.likeCount}{' '}
+              {pluralize(currentFeed?.data.likeCount || 0, 'user')}
+            </Text>
+            {isDesktopWeb && (
+              <View style={styles.headerBtns}>
+                <Button
+                  type={currentFeed?.isSaved ? 'default' : 'inverted'}
+                  onPress={onToggleSaved}
+                  accessibilityLabel={
+                    currentFeed?.isSaved ? 'Unsave this feed' : 'Save this feed'
+                  }
+                  accessibilityHint=""
+                  label={currentFeed?.isSaved ? 'Unsave' : 'Save'}
+                />
+
+                <Button type="default" onPress={onToggleLiked}>
+                  {currentFeed?.isLiked ? (
+                    <HeartIconSolid size={18} style={styles.liked} />
+                  ) : (
+                    <HeartIcon strokeWidth={3} size={18} style={pal.icon} />
+                  )}
+                </Button>
+              </View>
+            )}
+          </View>
+          <View style={[styles.fakeSelector, pal.border]}>
+            <View
+              style={[styles.fakeSelectorItem, {borderColor: pal.colors.link}]}>
+              <Text type="md-medium" style={[pal.text]}>
+                Feed
+              </Text>
+            </View>
+          </View>
+        </>
+      )
+    }, [store.me.did, pal, currentFeed, onToggleLiked, onToggleSaved])
+
+    return (
+      <View style={s.hContentRegion}>
+        <ViewHeader title="" renderButton={renderHeaderBtns} />
+        <Feed
+          scrollElRef={scrollElRef}
+          feed={algoFeed}
+          ListHeaderComponent={renderListHeaderComponent}
+          extraData={uri}
+        />
+      </View>
+    )
+  }),
+)
+
+/*
+
+        <View style={[styles.headerContainer]}>
+          <View style={[styles.header]}>
+            <View style={styles.avatarContainer}>
+              <UserAvatar
+                type="algo"
+                size={28}
+                avatar={currentFeed?.data.avatar}
+              />
+              <Link href={`/profile/${currentFeed?.data.creator.handle}`}>
+                <Text style={[pal.textLight]}>
+                  @{currentFeed?.data.creator.handle}
+                </Text>
+              </Link>
+            </View>
+            <Text style={[pal.text]}>{currentFeed?.data.description}</Text>
+          </View>
+
+          <View style={[styles.buttonsContainer]}>
+          </View>
+        </View>
+        */
+
+const styles = StyleSheet.create({
+  headerBtns: {
+    flexDirection: 'row',
+    gap: 8,
+  },
+  header: {
+    flexDirection: 'row',
+    gap: 12,
+    paddingHorizontal: 16,
+    paddingTop: 12,
+    paddingBottom: 16,
+    borderTopWidth: 1,
+  },
+  headerDetails: {
+    paddingHorizontal: 16,
+    paddingBottom: 16,
+  },
+  fakeSelector: {
+    flexDirection: 'row',
+    paddingHorizontal: isDesktopWeb ? 16 : 6,
+  },
+  fakeSelectorItem: {
+    paddingHorizontal: 12,
+    paddingBottom: 8,
+    borderBottomWidth: 3,
+  },
+  liked: {
+    color: colors.red3,
+  },
+
+  /*  headerContainer: {
+    alignItems: 'center',
+    justifyContent: 'center',
+    gap: 8,
+    marginBottom: 12,
+  },
+  header: {
+    alignItems: 'center',
+    gap: 4,
+  },
+  avatarContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 8,
+  },
+  buttonsContainer: {
+    flexDirection: 'row',
+    gap: 8,
+  },
+  saveButton: {
+    minWidth: 100,
+    alignItems: 'center',
+  },
+  liked: {
+    color: colors.red3,
+  },
+  notLiked: {
+    color: colors.gray3,
+  },
+  likeButton: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    paddingVertical: 4,
+    paddingHorizontal: 8,
+    borderRadius: 24,
+    gap: 4,
+  },*/
+})
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx
index 01f27bae1..7c3ed831c 100644
--- a/src/view/screens/ProfileList.tsx
+++ b/src/view/screens/ProfileList.tsx
@@ -87,7 +87,7 @@ export const ProfileListScreen = withAuthRequired(
       return <EmptyState icon="users-slash" message="This list is empty!" />
     }, [])
 
-    const renderHeaderBtn = React.useCallback(() => {
+    const renderHeaderBtns = React.useCallback(() => {
       return (
         <View style={styles.headerBtns}>
           {list?.isOwner && (
@@ -148,7 +148,7 @@ export const ProfileListScreen = withAuthRequired(
           pal.border,
         ]}
         testID="moderationMutelistsScreen">
-        <ViewHeader title="" renderButton={renderHeaderBtn} />
+        <ViewHeader title="" renderButton={renderHeaderBtns} />
         <ListItems
           list={list}
           renderEmptyState={renderEmptyState}
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx
index 6d55649f7..c32639889 100644
--- a/src/view/screens/SavedFeeds.tsx
+++ b/src/view/screens/SavedFeeds.tsx
@@ -137,12 +137,9 @@ const ListHeaderComponent = observer(
                     key={item.data.uri}
                     accessibilityRole="button"
                     onPress={() => {
-                      navigation.navigate('CustomFeed', {
+                      navigation.navigate('ProfileCustomFeed', {
                         name: item.data.creator.did,
                         rkey: new AtUri(item.data.uri).rkey,
-                        displayName:
-                          item.data.displayName ??
-                          `${item.data.creator.displayName}'s feed`,
                       })
                     }}
                     style={styles.pinnedItem}>
-- 
cgit 1.4.1


From f0003d193182bd70935ca6b7e67897922297deed Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Wed, 17 May 2023 20:33:58 -0500
Subject: Add 'my feeds' tab

---
 src/state/models/ui/saved-feeds.ts       |  44 +++++----
 src/view/com/feeds/CustomFeed.tsx        |   4 +-
 src/view/com/feeds/SavedFeeds.tsx        | 163 +++++++++++++++++++++++++++++++
 src/view/com/pager/FeedsTabBarMobile.tsx |   1 +
 src/view/screens/Home.tsx                |  10 +-
 5 files changed, 198 insertions(+), 24 deletions(-)
 create mode 100644 src/view/com/feeds/SavedFeeds.tsx

(limited to 'src/view/com/feeds/CustomFeed.tsx')

diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts
index dca079b72..bae98fc84 100644
--- a/src/state/models/ui/saved-feeds.ts
+++ b/src/state/models/ui/saved-feeds.ts
@@ -139,8 +139,8 @@ export class SavedFeedsModel {
   // public api
   // =
 
-  async refresh() {
-    return this.loadMore(true)
+  async refresh(quietRefresh = false) {
+    return this.loadMore(true, quietRefresh)
   }
 
   clear() {
@@ -153,26 +153,28 @@ export class SavedFeedsModel {
     this.feeds = []
   }
 
-  loadMore = bundleAsync(async (replace: boolean = false) => {
-    if (!replace && !this.hasMore) {
-      return
-    }
-    this._xLoading(replace)
-    try {
-      const res = await this.rootStore.agent.app.bsky.feed.getSavedFeeds({
-        limit: PAGE_SIZE,
-        cursor: replace ? undefined : this.loadMoreCursor,
-      })
-      if (replace) {
-        this._replaceAll(res)
-      } else {
-        this._appendAll(res)
+  loadMore = bundleAsync(
+    async (replace: boolean = false, quietRefresh = false) => {
+      if (!replace && !this.hasMore) {
+        return
       }
-      this._xIdle()
-    } catch (e: any) {
-      this._xIdle(e)
-    }
-  })
+      this._xLoading(replace && !quietRefresh)
+      try {
+        const res = await this.rootStore.agent.app.bsky.feed.getSavedFeeds({
+          limit: PAGE_SIZE,
+          cursor: replace ? undefined : this.loadMoreCursor,
+        })
+        if (replace) {
+          this._replaceAll(res)
+        } else {
+          this._appendAll(res)
+        }
+        this._xIdle()
+      } catch (e: any) {
+        this._xIdle(e)
+      }
+    },
+  )
 
   removeFeed(uri: string) {
     this.feeds = this.feeds.filter(f => f.data.uri !== uri)
diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx
index 5a93020a0..5440a8e8f 100644
--- a/src/view/com/feeds/CustomFeed.tsx
+++ b/src/view/com/feeds/CustomFeed.tsx
@@ -24,11 +24,13 @@ export const CustomFeed = observer(
     item,
     style,
     showSaveBtn = false,
+    showDescription = false,
     showLikes = false,
   }: {
     item: CustomFeedModel
     style?: StyleProp<ViewStyle>
     showSaveBtn?: boolean
+    showDescription?: boolean
     showLikes?: boolean
   }) => {
     const store = useStores()
@@ -75,7 +77,7 @@ export const CustomFeed = observer(
           )}
         </View>
 
-        {item.data.description ? (
+        {showDescription && item.data.description ? (
           <Text style={[pal.textLight, styles.description]} numberOfLines={3}>
             {item.data.description}
           </Text>
diff --git a/src/view/com/feeds/SavedFeeds.tsx b/src/view/com/feeds/SavedFeeds.tsx
new file mode 100644
index 000000000..66a4efecf
--- /dev/null
+++ b/src/view/com/feeds/SavedFeeds.tsx
@@ -0,0 +1,163 @@
+import React, {useEffect, useCallback} from 'react'
+import {
+  ActivityIndicator,
+  FlatList,
+  RefreshControl,
+  StyleSheet,
+  TouchableOpacity,
+  View,
+} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {usePalette} from 'lib/hooks/usePalette'
+import {observer} from 'mobx-react-lite'
+import {useStores} from 'state/index'
+import {CustomFeedModel} from 'state/models/feeds/custom-feed'
+import {SavedFeedsModel} from 'state/models/ui/saved-feeds'
+import {CenteredView} from 'view/com/util/Views'
+import {Text} from 'view/com/util/text/Text'
+import {isDesktopWeb} from 'platform/detection'
+import {s, colors} from 'lib/styles'
+import {Link} from 'view/com/util/Link'
+import {CustomFeed} from 'view/com/feeds/CustomFeed'
+
+export const SavedFeeds = observer(
+  ({
+    headerOffset = 0,
+    isPageFocused,
+  }: {
+    headerOffset?: number
+    isPageFocused: boolean
+  }) => {
+    const pal = usePalette('default')
+    const store = useStores()
+
+    useEffect(() => {
+      if (isPageFocused) {
+        store.shell.setMinimalShellMode(false)
+        store.me.savedFeeds.refresh(true)
+      }
+    }, [store, isPageFocused])
+
+    const renderListEmptyComponent = useCallback(() => {
+      return (
+        <View
+          style={[
+            pal.border,
+            !isDesktopWeb && s.flex1,
+            pal.viewLight,
+            styles.empty,
+          ]}>
+          <Text type="lg" style={[pal.text]}>
+            You don't have any saved feeds. You can find feeds by searching on
+            Bluesky.
+          </Text>
+        </View>
+      )
+    }, [pal])
+
+    const renderListFooterComponent = useCallback(() => {
+      return (
+        <Link
+          style={[styles.footerLink, pal.border]}
+          href="/settings/pinned-feeds">
+          <FontAwesomeIcon icon="cog" size={18} color={pal.colors.icon} />
+          <Text type="lg-medium" style={pal.textLight}>
+            Settings
+          </Text>
+        </Link>
+      )
+    }, [pal])
+
+    const renderItem = useCallback(
+      ({item}) => (
+        <SavedFeedItem
+          key={item.data.uri}
+          item={item}
+          savedFeeds={store.me.savedFeeds}
+        />
+      ),
+      [store.me.savedFeeds],
+    )
+
+    return (
+      <CenteredView style={[s.flex1]}>
+        <FlatList
+          style={[!isDesktopWeb && s.flex1, {paddingTop: headerOffset}]}
+          data={store.me.savedFeeds.feeds}
+          keyExtractor={item => item.data.uri}
+          refreshing={store.me.savedFeeds.isRefreshing}
+          refreshControl={
+            <RefreshControl
+              refreshing={store.me.savedFeeds.isRefreshing}
+              onRefresh={() => store.me.savedFeeds.refresh()}
+              tintColor={pal.colors.text}
+              titleColor={pal.colors.text}
+              progressViewOffset={headerOffset}
+            />
+          }
+          renderItem={renderItem}
+          initialNumToRender={10}
+          ListFooterComponent={renderListFooterComponent}
+          ListEmptyComponent={renderListEmptyComponent}
+          extraData={store.me.savedFeeds.isLoading}
+          contentOffset={{x: 0, y: headerOffset * -1}}
+          // @ts-ignore our .web version only -prf
+          desktopFixedHeight
+        />
+      </CenteredView>
+    )
+  },
+)
+
+const SavedFeedItem = observer(
+  ({
+    item,
+    savedFeeds,
+  }: {
+    item: CustomFeedModel
+    savedFeeds: SavedFeedsModel
+  }) => {
+    const isPinned = savedFeeds.isPinned(item)
+    const onTogglePinned = useCallback(
+      () => savedFeeds.togglePinnedFeed(item),
+      [savedFeeds, item],
+    )
+
+    return (
+      <View style={styles.itemContainer}>
+        <CustomFeed key={item.data.uri} item={item} />
+        <TouchableOpacity accessibilityRole="button" onPress={onTogglePinned}>
+          <FontAwesomeIcon
+            icon="thumb-tack"
+            size={20}
+            color={isPinned ? colors.blue3 : colors.gray3}
+          />
+        </TouchableOpacity>
+      </View>
+    )
+  },
+)
+
+const styles = StyleSheet.create({
+  footerLink: {
+    flexDirection: 'row',
+    borderTopWidth: 1,
+    borderBottomWidth: 1,
+    paddingHorizontal: 26,
+    paddingVertical: 18,
+    gap: 18,
+  },
+  empty: {
+    paddingHorizontal: 18,
+    paddingVertical: 16,
+    borderRadius: 8,
+    marginHorizontal: 18,
+    marginTop: 10,
+  },
+  itemContainer: {
+    flex: 1,
+    flexDirection: 'row',
+    alignItems: 'center',
+    marginRight: 18,
+  },
+})
diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx
index ab8f98309..c79dad4df 100644
--- a/src/view/com/pager/FeedsTabBarMobile.tsx
+++ b/src/view/com/pager/FeedsTabBarMobile.tsx
@@ -37,6 +37,7 @@ export const FeedsTabBar = observer(
         'Following',
         "What's hot",
         ...store.me.savedFeeds.listOfPinnedFeedNames,
+        'My feeds',
       ],
       [store.me.savedFeeds.listOfPinnedFeedNames],
     )
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 70816a306..9be4a4794 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -14,6 +14,7 @@ import {LoadLatestBtn} from '../com/util/load-latest/LoadLatestBtn'
 import {FeedsTabBar} from '../com/pager/FeedsTabBar'
 import {Pager, RenderTabBarFnProps} from 'view/com/pager/Pager'
 import {FAB} from '../com/util/fab/FAB'
+import {SavedFeeds} from 'view/com/feeds/SavedFeeds'
 import {useStores} from 'state/index'
 import {s} from 'lib/styles'
 import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
@@ -115,14 +116,19 @@ export const HomeScreen = withAuthRequired(
         {store.me.savedFeeds.pinned.map((f, index) => {
           return (
             <FeedPage
-              key={String(2 + index + 1)}
-              testID="customFeed"
+              key={String(3 + index)}
+              testID="customFeedPage"
               isPageFocused={selectedPage === 2 + index}
               feed={new PostsFeedModel(store, 'custom', {feed: f.uri})}
               renderEmptyState={renderFollowingEmptyState}
             />
           )
         })}
+        <SavedFeeds
+          key={String(3 + store.me.savedFeeds.pinned.length)}
+          headerOffset={HEADER_OFFSET}
+          isPageFocused={selectedPage === 2 + store.me.savedFeeds.pinned.length}
+        />
       </Pager>
     )
   }),
-- 
cgit 1.4.1


From 6bf8e7215784dafd04ae3639a28666eea77ed1bd Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Wed, 17 May 2023 21:23:32 -0500
Subject: Usability improvements to feeds

---
 src/state/models/ui/saved-feeds.ts       | 20 ++++-----
 src/view/com/feeds/CustomFeed.tsx        | 77 ++++++++++++++++++++------------
 src/view/com/feeds/SavedFeedItem.tsx     | 38 ++++++++++------
 src/view/com/feeds/SavedFeeds.tsx        | 61 +++----------------------
 src/view/com/pager/FeedsTabBar.web.tsx   |  8 +---
 src/view/com/pager/FeedsTabBarMobile.tsx |  4 +-
 src/view/screens/PinnedFeeds.tsx         | 34 +++++++-------
 src/view/screens/ProfileCustomFeed.tsx   | 18 ++++----
 8 files changed, 119 insertions(+), 141 deletions(-)

(limited to 'src/view/com/feeds/CustomFeed.tsx')

diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts
index bae98fc84..50bb1b871 100644
--- a/src/state/models/ui/saved-feeds.ts
+++ b/src/state/models/ui/saved-feeds.ts
@@ -61,22 +61,22 @@ export class SavedFeedsModel {
     return this.hasLoaded && !this.hasContent
   }
 
-  get numOfFeeds() {
+  get numFeeds() {
     return this.feeds.length
   }
 
-  get listOfFeedNames() {
-    return this.feeds.map(f => f.displayName)
+  get unpinned() {
+    return this.feeds.filter(
+      f => !this.pinned.find(p => p.data.uri === f.data.uri),
+    )
   }
 
-  get listOfPinnedFeedNames() {
-    return this.pinned.map(f => f.displayName)
+  get feedNames() {
+    return this.feeds.map(f => f.displayName)
   }
 
-  get savedFeedsWithoutPinned() {
-    return this.feeds.filter(
-      f => !this.pinned.find(p => p.data.uri === f.data.uri),
-    )
+  get pinnedFeedNames() {
+    return this.pinned.map(f => f.displayName)
   }
 
   togglePinnedFeed(feed: CustomFeedModel) {
@@ -92,7 +92,7 @@ export class SavedFeedsModel {
   }
 
   reorderPinnedFeeds(temp: CustomFeedModel[]) {
-    this.pinned = temp
+    this.pinned = temp.filter(item => this.isPinned(item))
   }
 
   isPinned(feed: CustomFeedModel) {
diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx
index 5440a8e8f..5201ca848 100644
--- a/src/view/com/feeds/CustomFeed.tsx
+++ b/src/view/com/feeds/CustomFeed.tsx
@@ -1,16 +1,17 @@
 import React from 'react'
 import {
+  Pressable,
   StyleProp,
   StyleSheet,
   View,
   ViewStyle,
   TouchableOpacity,
 } from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {Text} from '../util/text/Text'
 import {usePalette} from 'lib/hooks/usePalette'
 import {s} from 'lib/styles'
 import {UserAvatar} from '../util/UserAvatar'
-import {Button} from '../util/forms/Button'
 import {observer} from 'mobx-react-lite'
 import {CustomFeedModel} from 'state/models/feeds/custom-feed'
 import {useNavigation} from '@react-navigation/native'
@@ -18,6 +19,7 @@ import {NavigationProp} from 'lib/routes/types'
 import {useStores} from 'state/index'
 import {pluralize} from 'lib/strings/helpers'
 import {AtUri} from '@atproto/api'
+import * as Toast from 'view/com/util/Toast'
 
 export const CustomFeed = observer(
   ({
@@ -37,6 +39,23 @@ export const CustomFeed = observer(
     const pal = usePalette('default')
     const navigation = useNavigation<NavigationProp>()
 
+    const onToggleSaved = React.useCallback(() => {
+      if (item.data.viewer?.saved) {
+        store.shell.openModal({
+          name: 'confirm',
+          title: 'Remove from my feeds',
+          message: `Remove ${item.displayName} from my feeds?`,
+          onPressConfirm: () => {
+            store.me.savedFeeds.unsave(item)
+            Toast.show('Removed from my feeds')
+          },
+        })
+      } else {
+        store.me.savedFeeds.save(item)
+        Toast.show('Added to my feeds')
+      }
+    }, [store, item])
+
     return (
       <TouchableOpacity
         accessibilityRole="button"
@@ -62,17 +81,28 @@ export const CustomFeed = observer(
           </View>
           {showSaveBtn && (
             <View>
-              <Button
-                type={item.isSaved ? 'default' : 'inverted'}
-                onPress={() => {
-                  if (item.data.viewer?.saved) {
-                    store.me.savedFeeds.unsave(item)
-                  } else {
-                    store.me.savedFeeds.save(item)
-                  }
-                }}
-                label={item.data.viewer?.saved ? 'Unsave' : 'Save'}
-              />
+              <Pressable
+                accessibilityRole="button"
+                accessibilityLabel={
+                  item.isSaved ? 'Remove from my feeds' : 'Add to my feeds'
+                }
+                accessibilityHint=""
+                onPress={onToggleSaved}
+                style={styles.btn}>
+                {item.isSaved ? (
+                  <FontAwesomeIcon
+                    icon={['far', 'trash-can']}
+                    size={19}
+                    color={pal.colors.icon}
+                  />
+                ) : (
+                  <FontAwesomeIcon
+                    icon="plus"
+                    size={18}
+                    color={pal.colors.link}
+                  />
+                )}
+              </Pressable>
             </View>
           )}
         </View>
@@ -84,14 +114,10 @@ export const CustomFeed = observer(
         ) : null}
 
         {showLikes ? (
-          <View style={styles.bottomContainer}>
-            <View style={styles.likedByContainer}>
-              <Text type="sm-medium" style={[pal.text, pal.textLight]}>
-                Liked by {item.data.likeCount || 0}{' '}
-                {pluralize(item.data.likeCount || 0, 'user')}
-              </Text>
-            </View>
-          </View>
+          <Text type="sm-medium" style={[pal.text, pal.textLight]}>
+            Liked by {item.data.likeCount || 0}{' '}
+            {pluralize(item.data.likeCount || 0, 'user')}
+          </Text>
         ) : null}
       </TouchableOpacity>
     )
@@ -119,14 +145,7 @@ const styles = StyleSheet.create({
     flex: 1,
     flexWrap: 'wrap',
   },
-  bottomContainer: {
-    flexDirection: 'row',
-    justifyContent: 'space-between',
-    alignItems: 'center',
-  },
-  likedByContainer: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    gap: 2,
+  btn: {
+    paddingVertical: 6,
   },
 })
diff --git a/src/view/com/feeds/SavedFeedItem.tsx b/src/view/com/feeds/SavedFeedItem.tsx
index 329f1811e..8a5889e47 100644
--- a/src/view/com/feeds/SavedFeedItem.tsx
+++ b/src/view/com/feeds/SavedFeedItem.tsx
@@ -1,41 +1,51 @@
-import React from 'react'
-import {View, TouchableOpacity, StyleSheet} from 'react-native'
+import React, {useCallback} from 'react'
+import {
+  View,
+  TouchableOpacity,
+  StyleSheet,
+  StyleProp,
+  ViewStyle,
+} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {colors} from 'lib/styles'
 import {observer} from 'mobx-react-lite'
 import {CustomFeedModel} from 'state/models/feeds/custom-feed'
 import {SavedFeedsModel} from 'state/models/ui/saved-feeds'
 import {CustomFeed} from './CustomFeed'
+import {usePalette} from 'lib/hooks/usePalette'
 
 export const SavedFeedItem = observer(
   ({
     item,
     savedFeeds,
+    showSaveBtn = false,
+    style,
   }: {
     item: CustomFeedModel
     savedFeeds: SavedFeedsModel
+    showSaveBtn?: boolean
+    style?: StyleProp<ViewStyle>
   }) => {
+    const pal = usePalette('default')
     const isPinned = savedFeeds.isPinned(item)
+    const onTogglePinned = useCallback(
+      () => savedFeeds.togglePinnedFeed(item),
+      [savedFeeds, item],
+    )
 
     return (
-      <View style={styles.itemContainer}>
+      <View style={[styles.itemContainer, style]}>
         <CustomFeed
           key={item.data.uri}
           item={item}
-          style={styles.item}
-          showSaveBtn
+          showSaveBtn={showSaveBtn}
+          style={styles.noBorder}
         />
-        <TouchableOpacity
-          accessibilityRole="button"
-          onPress={() => {
-            savedFeeds.togglePinnedFeed(item)
-            console.log('pinned', savedFeeds.pinned)
-            console.log('isPinned', savedFeeds.isPinned(item))
-          }}>
+        <TouchableOpacity accessibilityRole="button" onPress={onTogglePinned}>
           <FontAwesomeIcon
             icon="thumb-tack"
             size={20}
-            color={isPinned ? colors.blue3 : colors.gray3}
+            color={isPinned ? colors.blue3 : pal.colors.icon}
           />
         </TouchableOpacity>
       </View>
@@ -50,7 +60,7 @@ const styles = StyleSheet.create({
     alignItems: 'center',
     marginRight: 18,
   },
-  item: {
+  noBorder: {
     borderTopWidth: 0,
   },
 })
diff --git a/src/view/com/feeds/SavedFeeds.tsx b/src/view/com/feeds/SavedFeeds.tsx
index 66a4efecf..06c47d114 100644
--- a/src/view/com/feeds/SavedFeeds.tsx
+++ b/src/view/com/feeds/SavedFeeds.tsx
@@ -1,24 +1,15 @@
 import React, {useEffect, useCallback} from 'react'
-import {
-  ActivityIndicator,
-  FlatList,
-  RefreshControl,
-  StyleSheet,
-  TouchableOpacity,
-  View,
-} from 'react-native'
+import {FlatList, RefreshControl, StyleSheet, View} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {usePalette} from 'lib/hooks/usePalette'
 import {observer} from 'mobx-react-lite'
 import {useStores} from 'state/index'
-import {CustomFeedModel} from 'state/models/feeds/custom-feed'
-import {SavedFeedsModel} from 'state/models/ui/saved-feeds'
 import {CenteredView} from 'view/com/util/Views'
 import {Text} from 'view/com/util/text/Text'
 import {isDesktopWeb} from 'platform/detection'
-import {s, colors} from 'lib/styles'
+import {s} from 'lib/styles'
 import {Link} from 'view/com/util/Link'
-import {CustomFeed} from 'view/com/feeds/CustomFeed'
+import {CustomFeed} from './CustomFeed'
 
 export const SavedFeeds = observer(
   ({
@@ -69,14 +60,8 @@ export const SavedFeeds = observer(
     }, [pal])
 
     const renderItem = useCallback(
-      ({item}) => (
-        <SavedFeedItem
-          key={item.data.uri}
-          item={item}
-          savedFeeds={store.me.savedFeeds}
-        />
-      ),
-      [store.me.savedFeeds],
+      ({item}) => <CustomFeed key={item.data.uri} item={item} />,
+      [],
     )
 
     return (
@@ -109,35 +94,6 @@ export const SavedFeeds = observer(
   },
 )
 
-const SavedFeedItem = observer(
-  ({
-    item,
-    savedFeeds,
-  }: {
-    item: CustomFeedModel
-    savedFeeds: SavedFeedsModel
-  }) => {
-    const isPinned = savedFeeds.isPinned(item)
-    const onTogglePinned = useCallback(
-      () => savedFeeds.togglePinnedFeed(item),
-      [savedFeeds, item],
-    )
-
-    return (
-      <View style={styles.itemContainer}>
-        <CustomFeed key={item.data.uri} item={item} />
-        <TouchableOpacity accessibilityRole="button" onPress={onTogglePinned}>
-          <FontAwesomeIcon
-            icon="thumb-tack"
-            size={20}
-            color={isPinned ? colors.blue3 : colors.gray3}
-          />
-        </TouchableOpacity>
-      </View>
-    )
-  },
-)
-
 const styles = StyleSheet.create({
   footerLink: {
     flexDirection: 'row',
@@ -154,10 +110,7 @@ const styles = StyleSheet.create({
     marginHorizontal: 18,
     marginTop: 10,
   },
-  itemContainer: {
-    flex: 1,
-    flexDirection: 'row',
-    alignItems: 'center',
-    marginRight: 18,
+  feedItem: {
+    borderTopWidth: 1,
   },
 })
diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx
index 6de38fa1d..d1a05b153 100644
--- a/src/view/com/pager/FeedsTabBar.web.tsx
+++ b/src/view/com/pager/FeedsTabBar.web.tsx
@@ -28,12 +28,8 @@ const FeedsTabBarDesktop = observer(
   ) => {
     const store = useStores()
     const items = useMemo(
-      () => [
-        'Following',
-        "What's hot",
-        ...store.me.savedFeeds.listOfPinnedFeedNames,
-      ],
-      [store.me.savedFeeds.listOfPinnedFeedNames],
+      () => ['Following', "What's hot", ...store.me.savedFeeds.pinnedFeedNames],
+      [store.me.savedFeeds.pinnedFeedNames],
     )
     const pal = usePalette('default')
     const interp = useAnimatedValue(0)
diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx
index c79dad4df..0c40da436 100644
--- a/src/view/com/pager/FeedsTabBarMobile.tsx
+++ b/src/view/com/pager/FeedsTabBarMobile.tsx
@@ -36,10 +36,10 @@ export const FeedsTabBar = observer(
       () => [
         'Following',
         "What's hot",
-        ...store.me.savedFeeds.listOfPinnedFeedNames,
+        ...store.me.savedFeeds.pinnedFeedNames,
         'My feeds',
       ],
-      [store.me.savedFeeds.listOfPinnedFeedNames],
+      [store.me.savedFeeds.pinnedFeedNames],
     )
 
     return (
diff --git a/src/view/screens/PinnedFeeds.tsx b/src/view/screens/PinnedFeeds.tsx
index 85f737749..a90012093 100644
--- a/src/view/screens/PinnedFeeds.tsx
+++ b/src/view/screens/PinnedFeeds.tsx
@@ -72,10 +72,10 @@ export const PinnedFeeds = withAuthRequired(
 
     return (
       <CenteredView style={[s.flex1]}>
-        <ViewHeader title="Arrange Pinned Feeds" showOnDesktop />
+        <ViewHeader title="Edit My Feeds" showOnDesktop />
         <DraggableFlatList
           containerStyle={[!isDesktopWeb && s.flex1]}
-          data={[...savedFeeds.pinned]} // make a copy so this FlatList re-renders when pinned changes
+          data={[...savedFeeds.pinned, ...savedFeeds.unpinned]} // make a copy so this FlatList re-renders when pinned changes
           keyExtractor={item => item.data.uri}
           refreshing={savedFeeds.isRefreshing}
           refreshControl={
@@ -86,7 +86,7 @@ export const PinnedFeeds = withAuthRequired(
               titleColor={pal.colors.text}
             />
           }
-          renderItem={({item, drag}) => <PinnedItem item={item} drag={drag} />}
+          renderItem={({item, drag}) => <ListItem item={item} drag={drag} />}
           initialNumToRender={10}
           ListFooterComponent={_ListFooterComponent}
           ListEmptyComponent={_ListEmptyComponent}
@@ -100,19 +100,20 @@ export const PinnedFeeds = withAuthRequired(
   }),
 )
 
-const PinnedItem = observer(
+const ListItem = observer(
   ({item, drag}: {item: CustomFeedModel; drag: () => void}) => {
     const pal = usePalette('default')
     const rootStore = useStores()
     const savedFeeds = useMemo(() => rootStore.me.savedFeeds, [rootStore])
+    const isPinned = savedFeeds.isPinned(item)
     return (
       <ScaleDecorator>
         <ShadowDecorator>
           <Pressable
             accessibilityRole="button"
-            onLongPress={drag}
-            style={styles.itemContainer}>
-            {isWeb ? (
+            onLongPress={isPinned ? drag : undefined}
+            style={[styles.itemContainer, pal.border]}>
+            {isPinned && isWeb ? (
               <View style={styles.webArrowButtonsContainer}>
                 <TouchableOpacity
                   accessibilityRole="button"
@@ -122,7 +123,7 @@ const PinnedItem = observer(
                   <FontAwesomeIcon
                     icon="arrow-up"
                     size={20}
-                    style={[styles.icon, pal.text, styles.webArrowUpButton]}
+                    style={[s.mr10, pal.text, styles.webArrowUpButton]}
                   />
                 </TouchableOpacity>
                 <TouchableOpacity
@@ -133,18 +134,19 @@ const PinnedItem = observer(
                   <FontAwesomeIcon
                     icon="arrow-down"
                     size={20}
-                    style={[styles.icon, pal.text]}
+                    style={[s.mr10, pal.text]}
                   />
                 </TouchableOpacity>
               </View>
-            ) : (
+            ) : isPinned ? (
               <FontAwesomeIcon
                 icon="bars"
                 size={20}
-                style={[styles.icon, pal.text]}
+                color={pal.colors.text}
+                style={s.ml20}
               />
-            )}
-            <SavedFeedItem item={item} savedFeeds={savedFeeds} />
+            ) : null}
+            <SavedFeedItem item={item} savedFeeds={savedFeeds} showSaveBtn />
           </Pressable>
         </ShadowDecorator>
       </ScaleDecorator>
@@ -167,12 +169,8 @@ const styles = StyleSheet.create({
     flex: 1,
     flexDirection: 'row',
     alignItems: 'center',
-    marginLeft: 18,
+    borderTopWidth: 1,
   },
-  item: {
-    borderTopWidth: 0,
-  },
-  icon: {marginRight: 10},
   webArrowButtonsContainer: {
     flexDirection: 'column',
     justifyContent: 'space-around',
diff --git a/src/view/screens/ProfileCustomFeed.tsx b/src/view/screens/ProfileCustomFeed.tsx
index 1113ebf01..681798308 100644
--- a/src/view/screens/ProfileCustomFeed.tsx
+++ b/src/view/screens/ProfileCustomFeed.tsx
@@ -43,10 +43,10 @@ export const ProfileCustomFeed = withAuthRequired(
 
     const onToggleSaved = React.useCallback(async () => {
       try {
-        if (currentFeed.isSaved) {
-          await currentFeed.unsave()
+        if (currentFeed?.isSaved) {
+          await currentFeed?.unsave()
         } else {
-          await currentFeed.save()
+          await currentFeed?.save()
         }
       } catch (err) {
         Toast.show(
@@ -58,10 +58,10 @@ export const ProfileCustomFeed = withAuthRequired(
 
     const onToggleLiked = React.useCallback(async () => {
       try {
-        if (currentFeed.isLiked) {
-          await currentFeed.unlike()
+        if (currentFeed?.isLiked) {
+          await currentFeed?.unlike()
         } else {
-          await currentFeed.like()
+          await currentFeed?.like()
         }
       } catch (err) {
         Toast.show(
@@ -90,10 +90,12 @@ export const ProfileCustomFeed = withAuthRequired(
             type={currentFeed?.isSaved ? 'default' : 'inverted'}
             onPress={onToggleSaved}
             accessibilityLabel={
-              currentFeed?.isSaved ? 'Unsave this feed' : 'Save this feed'
+              currentFeed?.isSaved ? 'Remove from my feeds' : 'Add to my feeds'
             }
             accessibilityHint=""
-            label={currentFeed?.isSaved ? 'Unsave' : 'Save'}
+            label={
+              currentFeed?.isSaved ? 'Remove from My Feeds' : 'Add to My Feeds'
+            }
           />
         </View>
       )
-- 
cgit 1.4.1


From a2c89b47a135d83057b34d0b0b7a1fa7551e0ad2 Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Wed, 17 May 2023 22:12:14 -0500
Subject: Add custom feed liked by screen

---
 src/Navigation.tsx                     |   9 +-
 src/lib/routes/types.ts                |   3 +-
 src/routes.ts                          |   1 +
 src/view/com/feeds/CustomFeed.tsx      |   2 +-
 src/view/screens/CustomFeed.tsx        | 235 ++++++++++++++++++++++++++
 src/view/screens/CustomFeedLikedBy.tsx |  29 ++++
 src/view/screens/ProfileCustomFeed.tsx | 293 ---------------------------------
 7 files changed, 275 insertions(+), 297 deletions(-)
 create mode 100644 src/view/screens/CustomFeed.tsx
 create mode 100644 src/view/screens/CustomFeedLikedBy.tsx
 delete mode 100644 src/view/screens/ProfileCustomFeed.tsx

(limited to 'src/view/com/feeds/CustomFeed.tsx')

diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index ea36b0f20..d84167d63 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -40,7 +40,8 @@ import {SettingsScreen} from './view/screens/Settings'
 import {ProfileScreen} from './view/screens/Profile'
 import {ProfileFollowersScreen} from './view/screens/ProfileFollowers'
 import {ProfileFollowsScreen} from './view/screens/ProfileFollows'
-import {ProfileCustomFeed} from './view/screens/ProfileCustomFeed'
+import {CustomFeedScreen} from './view/screens/CustomFeed'
+import {CustomFeedLikedByScreen} from './view/screens/CustomFeedLikedBy'
 import {ProfileListScreen} from './view/screens/ProfileList'
 import {PostThreadScreen} from './view/screens/PostThread'
 import {PostLikedByScreen} from './view/screens/PostLikedBy'
@@ -126,7 +127,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
           title: title(`People followed by @${route.params.name}`),
         })}
       />
-      <Stack.Screen name="ProfileCustomFeed" component={ProfileCustomFeed} />
       <Stack.Screen
         name="ProfileList"
         component={ProfileListScreen}
@@ -147,6 +147,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
         component={PostRepostedByScreen}
         options={({route}) => ({title: title(`Post by @${route.params.name}`)})}
       />
+      <Stack.Screen name="CustomFeed" component={CustomFeedScreen} />
+      <Stack.Screen
+        name="CustomFeedLikedBy"
+        component={CustomFeedLikedByScreen}
+      />
       <Stack.Screen
         name="Debug"
         component={DebugScreen}
diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts
index 5c5185602..b714c10c0 100644
--- a/src/lib/routes/types.ts
+++ b/src/lib/routes/types.ts
@@ -13,11 +13,12 @@ export type CommonNavigatorParams = {
   Profile: {name: string; hideBackButton?: boolean}
   ProfileFollowers: {name: string}
   ProfileFollows: {name: string}
-  ProfileCustomFeed: {name: string; rkey: string}
   ProfileList: {name: string; rkey: string}
   PostThread: {name: string; rkey: string}
   PostLikedBy: {name: string; rkey: string}
   PostRepostedBy: {name: string; rkey: string}
+  CustomFeed: {name: string; rkey: string}
+  CustomFeedLikedBy: {name: string; rkey: string}
   Debug: undefined
   Log: undefined
   Support: undefined
diff --git a/src/routes.ts b/src/routes.ts
index c5dc4fb5b..9e3e3f7ea 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -17,6 +17,7 @@ export const router = new Router({
   PostLikedBy: '/profile/:name/post/:rkey/liked-by',
   PostRepostedBy: '/profile/:name/post/:rkey/reposted-by',
   CustomFeed: '/profile/:name/feed/:rkey',
+  CustomFeedLikedBy: '/profile/:name/feed/:rkey/liked-by',
   Debug: '/sys/debug',
   Log: '/sys/log',
   AppPasswords: '/settings/app-passwords',
diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx
index 5201ca848..911c33da4 100644
--- a/src/view/com/feeds/CustomFeed.tsx
+++ b/src/view/com/feeds/CustomFeed.tsx
@@ -61,7 +61,7 @@ export const CustomFeed = observer(
         accessibilityRole="button"
         style={[styles.container, pal.border, style]}
         onPress={() => {
-          navigation.navigate('ProfileCustomFeed', {
+          navigation.navigate('CustomFeed', {
             name: item.data.creator.did,
             rkey: new AtUri(item.data.uri).rkey,
           })
diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx
new file mode 100644
index 000000000..9f7f81691
--- /dev/null
+++ b/src/view/screens/CustomFeed.tsx
@@ -0,0 +1,235 @@
+import React, {useMemo, useRef} from 'react'
+import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {usePalette} from 'lib/hooks/usePalette'
+import {HeartIcon, HeartIconSolid} from 'lib/icons'
+import {CommonNavigatorParams} from 'lib/routes/types'
+import {makeRecordUri} from 'lib/strings/url-helpers'
+import {colors, s} from 'lib/styles'
+import {observer} from 'mobx-react-lite'
+import {FlatList, StyleSheet, View} from 'react-native'
+import {useStores} from 'state/index'
+import {PostsFeedModel} from 'state/models/feeds/posts'
+import {useCustomFeed} from 'lib/hooks/useCustomFeed'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
+import {Feed} from 'view/com/posts/Feed'
+import {pluralize} from 'lib/strings/helpers'
+import {TextLink} from 'view/com/util/Link'
+import {UserAvatar} from 'view/com/util/UserAvatar'
+import {ViewHeader} from 'view/com/util/ViewHeader'
+import {Button} from 'view/com/util/forms/Button'
+import {Text} from 'view/com/util/text/Text'
+import * as Toast from 'view/com/util/Toast'
+import {isDesktopWeb} from 'platform/detection'
+
+type Props = NativeStackScreenProps<CommonNavigatorParams, 'CustomFeed'>
+export const CustomFeedScreen = withAuthRequired(
+  observer(({route}: Props) => {
+    const store = useStores()
+    const pal = usePalette('default')
+    const {rkey, name} = route.params
+    const uri = useMemo(
+      () => makeRecordUri(name, 'app.bsky.feed.generator', rkey),
+      [rkey, name],
+    )
+    const scrollElRef = useRef<FlatList>(null)
+    const currentFeed = useCustomFeed(uri)
+    const algoFeed: PostsFeedModel = useMemo(() => {
+      const feed = new PostsFeedModel(store, 'custom', {
+        feed: uri,
+      })
+      feed.setup()
+      return feed
+    }, [store, uri])
+
+    const onToggleSaved = React.useCallback(async () => {
+      try {
+        if (currentFeed?.isSaved) {
+          await currentFeed?.unsave()
+        } else {
+          await currentFeed?.save()
+        }
+      } catch (err) {
+        Toast.show(
+          'There was an an issue updating your feeds, please check your internet connection and try again.',
+        )
+        store.log.error('Failed up update feeds', {err})
+      }
+    }, [store, currentFeed])
+
+    const onToggleLiked = React.useCallback(async () => {
+      try {
+        if (currentFeed?.isLiked) {
+          await currentFeed?.unlike()
+        } else {
+          await currentFeed?.like()
+        }
+      } catch (err) {
+        Toast.show(
+          'There was an an issue contacting the server, please check your internet connection and try again.',
+        )
+        store.log.error('Failed up toggle like', {err})
+      }
+    }, [store, currentFeed])
+
+    const renderHeaderBtns = React.useCallback(() => {
+      return (
+        <View style={styles.headerBtns}>
+          <Button
+            type="default"
+            testID="toggleLikeBtn"
+            accessibilityLabel="Like this feed"
+            accessibilityHint=""
+            onPress={onToggleLiked}>
+            {currentFeed?.isLiked ? (
+              <HeartIconSolid size={18} style={styles.liked} />
+            ) : (
+              <HeartIcon strokeWidth={3} size={18} style={pal.textLight} />
+            )}
+          </Button>
+          <Button
+            type={currentFeed?.isSaved ? 'default' : 'inverted'}
+            onPress={onToggleSaved}
+            accessibilityLabel={
+              currentFeed?.isSaved ? 'Remove from my feeds' : 'Add to my feeds'
+            }
+            accessibilityHint=""
+            label={
+              currentFeed?.isSaved ? 'Remove from My Feeds' : 'Add to My Feeds'
+            }
+          />
+        </View>
+      )
+    }, [
+      pal,
+      currentFeed?.isSaved,
+      currentFeed?.isLiked,
+      onToggleSaved,
+      onToggleLiked,
+    ])
+
+    const renderListHeaderComponent = React.useCallback(() => {
+      return (
+        <>
+          <View style={[styles.header, pal.border]}>
+            <View style={s.flex1}>
+              <Text
+                testID="feedName"
+                type="title-xl"
+                style={[pal.text, s.bold]}>
+                {currentFeed?.displayName}
+              </Text>
+              {currentFeed && (
+                <Text type="md" style={[pal.textLight]} numberOfLines={1}>
+                  by{' '}
+                  {currentFeed.data.creator.did === store.me.did ? (
+                    'you'
+                  ) : (
+                    <TextLink
+                      text={`@${currentFeed.data.creator.handle}`}
+                      href={`/profile/${currentFeed.data.creator.did}`}
+                    />
+                  )}
+                </Text>
+              )}
+            </View>
+            <View>
+              <UserAvatar
+                type="algo"
+                avatar={currentFeed?.data.avatar}
+                size={64}
+              />
+            </View>
+          </View>
+          <View style={styles.headerDetails}>
+            {currentFeed?.data.description ? (
+              <Text style={[pal.text, s.mb10]} numberOfLines={6}>
+                {currentFeed.data.description}
+              </Text>
+            ) : null}
+            <TextLink
+              type="md-medium"
+              style={pal.textLight}
+              href={`/profile/${name}/feed/${rkey}/liked-by`}
+              text={`Liked by ${currentFeed?.data.likeCount} ${pluralize(
+                currentFeed?.data.likeCount || 0,
+                'user',
+              )}`}
+            />
+            {isDesktopWeb && (
+              <View style={styles.headerBtns}>
+                <Button
+                  type={currentFeed?.isSaved ? 'default' : 'inverted'}
+                  onPress={onToggleSaved}
+                  accessibilityLabel={
+                    currentFeed?.isSaved ? 'Unsave this feed' : 'Save this feed'
+                  }
+                  accessibilityHint=""
+                  label={currentFeed?.isSaved ? 'Unsave' : 'Save'}
+                />
+
+                <Button type="default" onPress={onToggleLiked}>
+                  {currentFeed?.isLiked ? (
+                    <HeartIconSolid size={18} style={styles.liked} />
+                  ) : (
+                    <HeartIcon strokeWidth={3} size={18} style={pal.icon} />
+                  )}
+                </Button>
+              </View>
+            )}
+          </View>
+          <View style={[styles.fakeSelector, pal.border]}>
+            <View
+              style={[styles.fakeSelectorItem, {borderColor: pal.colors.link}]}>
+              <Text type="md-medium" style={[pal.text]}>
+                Feed
+              </Text>
+            </View>
+          </View>
+        </>
+      )
+    }, [store.me.did, pal, currentFeed, onToggleLiked, onToggleSaved])
+
+    return (
+      <View style={s.hContentRegion}>
+        <ViewHeader title="" renderButton={renderHeaderBtns} />
+        <Feed
+          scrollElRef={scrollElRef}
+          feed={algoFeed}
+          ListHeaderComponent={renderListHeaderComponent}
+          extraData={uri}
+        />
+      </View>
+    )
+  }),
+)
+
+const styles = StyleSheet.create({
+  headerBtns: {
+    flexDirection: 'row',
+    gap: 8,
+  },
+  header: {
+    flexDirection: 'row',
+    gap: 12,
+    paddingHorizontal: 16,
+    paddingTop: 12,
+    paddingBottom: 16,
+    borderTopWidth: 1,
+  },
+  headerDetails: {
+    paddingHorizontal: 16,
+    paddingBottom: 16,
+  },
+  fakeSelector: {
+    flexDirection: 'row',
+    paddingHorizontal: isDesktopWeb ? 16 : 6,
+  },
+  fakeSelectorItem: {
+    paddingHorizontal: 12,
+    paddingBottom: 8,
+    borderBottomWidth: 3,
+  },
+  liked: {
+    color: colors.red3,
+  },
+})
diff --git a/src/view/screens/CustomFeedLikedBy.tsx b/src/view/screens/CustomFeedLikedBy.tsx
new file mode 100644
index 000000000..49d0d0482
--- /dev/null
+++ b/src/view/screens/CustomFeedLikedBy.tsx
@@ -0,0 +1,29 @@
+import React from 'react'
+import {View} from 'react-native'
+import {useFocusEffect} from '@react-navigation/native'
+import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
+import {ViewHeader} from '../com/util/ViewHeader'
+import {PostLikedBy as PostLikedByComponent} from '../com/post-thread/PostLikedBy'
+import {useStores} from 'state/index'
+import {makeRecordUri} from 'lib/strings/url-helpers'
+
+type Props = NativeStackScreenProps<CommonNavigatorParams, 'CustomFeedLikedBy'>
+export const CustomFeedLikedByScreen = withAuthRequired(({route}: Props) => {
+  const store = useStores()
+  const {name, rkey} = route.params
+  const uri = makeRecordUri(name, 'app.bsky.feed.generator', rkey)
+
+  useFocusEffect(
+    React.useCallback(() => {
+      store.shell.setMinimalShellMode(false)
+    }, [store]),
+  )
+
+  return (
+    <View>
+      <ViewHeader title="Liked by" />
+      <PostLikedByComponent uri={uri} />
+    </View>
+  )
+})
diff --git a/src/view/screens/ProfileCustomFeed.tsx b/src/view/screens/ProfileCustomFeed.tsx
deleted file mode 100644
index 681798308..000000000
--- a/src/view/screens/ProfileCustomFeed.tsx
+++ /dev/null
@@ -1,293 +0,0 @@
-import React, {useMemo, useRef} from 'react'
-import {NativeStackScreenProps} from '@react-navigation/native-stack'
-import {usePalette} from 'lib/hooks/usePalette'
-import {HeartIcon, HeartIconSolid} from 'lib/icons'
-import {CommonNavigatorParams} from 'lib/routes/types'
-import {makeRecordUri} from 'lib/strings/url-helpers'
-import {colors, s} from 'lib/styles'
-import {observer} from 'mobx-react-lite'
-import {FlatList, StyleSheet, View} from 'react-native'
-import {useStores} from 'state/index'
-import {PostsFeedModel} from 'state/models/feeds/posts'
-import {useCustomFeed} from 'lib/hooks/useCustomFeed'
-import {withAuthRequired} from 'view/com/auth/withAuthRequired'
-import {Feed} from 'view/com/posts/Feed'
-import {pluralize} from 'lib/strings/helpers'
-import {TextLink} from 'view/com/util/Link'
-import {UserAvatar} from 'view/com/util/UserAvatar'
-import {ViewHeader} from 'view/com/util/ViewHeader'
-import {Button} from 'view/com/util/forms/Button'
-import {Text} from 'view/com/util/text/Text'
-import * as Toast from 'view/com/util/Toast'
-import {isDesktopWeb} from 'platform/detection'
-
-type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileCustomFeed'>
-export const ProfileCustomFeed = withAuthRequired(
-  observer(({route}: Props) => {
-    const store = useStores()
-    const pal = usePalette('default')
-    const {rkey, name} = route.params
-    const uri = useMemo(
-      () => makeRecordUri(name, 'app.bsky.feed.generator', rkey),
-      [rkey, name],
-    )
-    const scrollElRef = useRef<FlatList>(null)
-    const currentFeed = useCustomFeed(uri)
-    const algoFeed: PostsFeedModel = useMemo(() => {
-      const feed = new PostsFeedModel(store, 'custom', {
-        feed: uri,
-      })
-      feed.setup()
-      return feed
-    }, [store, uri])
-
-    const onToggleSaved = React.useCallback(async () => {
-      try {
-        if (currentFeed?.isSaved) {
-          await currentFeed?.unsave()
-        } else {
-          await currentFeed?.save()
-        }
-      } catch (err) {
-        Toast.show(
-          'There was an an issue updating your feeds, please check your internet connection and try again.',
-        )
-        store.log.error('Failed up update feeds', {err})
-      }
-    }, [store, currentFeed])
-
-    const onToggleLiked = React.useCallback(async () => {
-      try {
-        if (currentFeed?.isLiked) {
-          await currentFeed?.unlike()
-        } else {
-          await currentFeed?.like()
-        }
-      } catch (err) {
-        Toast.show(
-          'There was an an issue contacting the server, please check your internet connection and try again.',
-        )
-        store.log.error('Failed up toggle like', {err})
-      }
-    }, [store, currentFeed])
-
-    const renderHeaderBtns = React.useCallback(() => {
-      return (
-        <View style={styles.headerBtns}>
-          <Button
-            type="default"
-            testID="toggleLikeBtn"
-            accessibilityLabel="Like this feed"
-            accessibilityHint=""
-            onPress={onToggleLiked}>
-            {currentFeed?.isLiked ? (
-              <HeartIconSolid size={18} style={styles.liked} />
-            ) : (
-              <HeartIcon strokeWidth={3} size={18} style={pal.textLight} />
-            )}
-          </Button>
-          <Button
-            type={currentFeed?.isSaved ? 'default' : 'inverted'}
-            onPress={onToggleSaved}
-            accessibilityLabel={
-              currentFeed?.isSaved ? 'Remove from my feeds' : 'Add to my feeds'
-            }
-            accessibilityHint=""
-            label={
-              currentFeed?.isSaved ? 'Remove from My Feeds' : 'Add to My Feeds'
-            }
-          />
-        </View>
-      )
-    }, [
-      pal,
-      currentFeed?.isSaved,
-      currentFeed?.isLiked,
-      onToggleSaved,
-      onToggleLiked,
-    ])
-
-    const renderListHeaderComponent = React.useCallback(() => {
-      return (
-        <>
-          <View style={[styles.header, pal.border]}>
-            <View style={s.flex1}>
-              <Text
-                testID="feedName"
-                type="title-xl"
-                style={[pal.text, s.bold]}>
-                {currentFeed?.displayName}
-              </Text>
-              {currentFeed && (
-                <Text type="md" style={[pal.textLight]} numberOfLines={1}>
-                  by{' '}
-                  {currentFeed.data.creator.did === store.me.did ? (
-                    'you'
-                  ) : (
-                    <TextLink
-                      text={`@${currentFeed.data.creator.handle}`}
-                      href={`/profile/${currentFeed.data.creator.did}`}
-                    />
-                  )}
-                </Text>
-              )}
-            </View>
-            <View>
-              <UserAvatar
-                type="algo"
-                avatar={currentFeed?.data.avatar}
-                size={64}
-              />
-            </View>
-          </View>
-          <View style={styles.headerDetails}>
-            {currentFeed?.data.description ? (
-              <Text style={[pal.text, s.mb10]} numberOfLines={6}>
-                {currentFeed.data.description}
-              </Text>
-            ) : null}
-            <Text type="md-medium" style={pal.textLight}>
-              Liked by {currentFeed?.data.likeCount}{' '}
-              {pluralize(currentFeed?.data.likeCount || 0, 'user')}
-            </Text>
-            {isDesktopWeb && (
-              <View style={styles.headerBtns}>
-                <Button
-                  type={currentFeed?.isSaved ? 'default' : 'inverted'}
-                  onPress={onToggleSaved}
-                  accessibilityLabel={
-                    currentFeed?.isSaved ? 'Unsave this feed' : 'Save this feed'
-                  }
-                  accessibilityHint=""
-                  label={currentFeed?.isSaved ? 'Unsave' : 'Save'}
-                />
-
-                <Button type="default" onPress={onToggleLiked}>
-                  {currentFeed?.isLiked ? (
-                    <HeartIconSolid size={18} style={styles.liked} />
-                  ) : (
-                    <HeartIcon strokeWidth={3} size={18} style={pal.icon} />
-                  )}
-                </Button>
-              </View>
-            )}
-          </View>
-          <View style={[styles.fakeSelector, pal.border]}>
-            <View
-              style={[styles.fakeSelectorItem, {borderColor: pal.colors.link}]}>
-              <Text type="md-medium" style={[pal.text]}>
-                Feed
-              </Text>
-            </View>
-          </View>
-        </>
-      )
-    }, [store.me.did, pal, currentFeed, onToggleLiked, onToggleSaved])
-
-    return (
-      <View style={s.hContentRegion}>
-        <ViewHeader title="" renderButton={renderHeaderBtns} />
-        <Feed
-          scrollElRef={scrollElRef}
-          feed={algoFeed}
-          ListHeaderComponent={renderListHeaderComponent}
-          extraData={uri}
-        />
-      </View>
-    )
-  }),
-)
-
-/*
-
-        <View style={[styles.headerContainer]}>
-          <View style={[styles.header]}>
-            <View style={styles.avatarContainer}>
-              <UserAvatar
-                type="algo"
-                size={28}
-                avatar={currentFeed?.data.avatar}
-              />
-              <Link href={`/profile/${currentFeed?.data.creator.handle}`}>
-                <Text style={[pal.textLight]}>
-                  @{currentFeed?.data.creator.handle}
-                </Text>
-              </Link>
-            </View>
-            <Text style={[pal.text]}>{currentFeed?.data.description}</Text>
-          </View>
-
-          <View style={[styles.buttonsContainer]}>
-          </View>
-        </View>
-        */
-
-const styles = StyleSheet.create({
-  headerBtns: {
-    flexDirection: 'row',
-    gap: 8,
-  },
-  header: {
-    flexDirection: 'row',
-    gap: 12,
-    paddingHorizontal: 16,
-    paddingTop: 12,
-    paddingBottom: 16,
-    borderTopWidth: 1,
-  },
-  headerDetails: {
-    paddingHorizontal: 16,
-    paddingBottom: 16,
-  },
-  fakeSelector: {
-    flexDirection: 'row',
-    paddingHorizontal: isDesktopWeb ? 16 : 6,
-  },
-  fakeSelectorItem: {
-    paddingHorizontal: 12,
-    paddingBottom: 8,
-    borderBottomWidth: 3,
-  },
-  liked: {
-    color: colors.red3,
-  },
-
-  /*  headerContainer: {
-    alignItems: 'center',
-    justifyContent: 'center',
-    gap: 8,
-    marginBottom: 12,
-  },
-  header: {
-    alignItems: 'center',
-    gap: 4,
-  },
-  avatarContainer: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    gap: 8,
-  },
-  buttonsContainer: {
-    flexDirection: 'row',
-    gap: 8,
-  },
-  saveButton: {
-    minWidth: 100,
-    alignItems: 'center',
-  },
-  liked: {
-    color: colors.red3,
-  },
-  notLiked: {
-    color: colors.gray3,
-  },
-  likeButton: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'center',
-    paddingVertical: 4,
-    paddingHorizontal: 8,
-    borderRadius: 24,
-    gap: 4,
-  },*/
-})
-- 
cgit 1.4.1


From 7691fe4f481bf08c711cf92da91b2c204d121a7f Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Thu, 18 May 2023 11:51:25 -0500
Subject: Store/sync pinned feeds on the server

---
 src/state/models/me.ts             |   7 +-
 src/state/models/ui/preferences.ts |  58 +++++++++-
 src/state/models/ui/saved-feeds.ts | 226 +++++++++++++------------------------
 src/view/com/feeds/CustomFeed.tsx  |  22 +++-
 src/view/com/feeds/SavedFeeds.tsx  |   6 +-
 src/view/com/pager/Pager.web.tsx   |  99 ++++++++--------
 src/view/screens/Home.tsx          |  16 ++-
 src/view/screens/SavedFeeds.tsx    |  84 +++++++++-----
 8 files changed, 278 insertions(+), 240 deletions(-)

(limited to 'src/view/com/feeds/CustomFeed.tsx')

diff --git a/src/state/models/me.ts b/src/state/models/me.ts
index 9b2b96832..815044857 100644
--- a/src/state/models/me.ts
+++ b/src/state/models/me.ts
@@ -69,7 +69,6 @@ export class MeModel {
       displayName: this.displayName,
       description: this.description,
       avatar: this.avatar,
-      savedFeeds: this.savedFeeds.serialize(),
     }
   }
 
@@ -91,9 +90,6 @@ export class MeModel {
       if (hasProp(v, 'avatar') && typeof v.avatar === 'string') {
         avatar = v.avatar
       }
-      if (hasProp(v, 'savedFeeds') && isObj(v.savedFeeds)) {
-        this.savedFeeds.hydrate(v.savedFeeds)
-      }
       if (did && handle) {
         this.did = did
         this.handle = handle
@@ -118,7 +114,7 @@ export class MeModel {
       /* dont await */ this.notifications.setup().catch(e => {
         this.rootStore.log.error('Failed to setup notifications model', e)
       })
-      /* dont await */ this.savedFeeds.refresh()
+      /* dont await */ this.savedFeeds.refresh(true)
       this.rootStore.emitSessionLoaded()
       await this.fetchInviteCodes()
       await this.fetchAppPasswords()
@@ -128,6 +124,7 @@ export class MeModel {
   }
 
   async updateIfNeeded() {
+    /* dont await */ this.savedFeeds.refresh(true)
     if (Date.now() - this.lastProfileStateUpdate > PROFILE_UPDATE_INTERVAL) {
       this.rootStore.log.debug('Updating me profile information')
       this.lastProfileStateUpdate = Date.now()
diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts
index 1471420fc..05a1eb128 100644
--- a/src/state/models/ui/preferences.ts
+++ b/src/state/models/ui/preferences.ts
@@ -25,6 +25,7 @@ const LABEL_GROUPS = [
   'spam',
   'impersonation',
 ]
+const VISIBILITY_VALUES = ['show', 'warn', 'hide']
 
 export class LabelPreferencesModel {
   nsfw: LabelPreference = 'hide'
@@ -45,6 +46,7 @@ export class PreferencesModel {
   contentLanguages: string[] =
     deviceLocales?.map?.(locale => locale.languageCode) || []
   contentLabels = new LabelPreferencesModel()
+  pinnedFeeds: string[] = []
 
   constructor(public rootStore: RootStoreModel) {
     makeAutoObservable(this, {}, {autoBind: true})
@@ -54,6 +56,7 @@ export class PreferencesModel {
     return {
       contentLanguages: this.contentLanguages,
       contentLabels: this.contentLabels,
+      pinnedFeeds: this.pinnedFeeds,
     }
   }
 
@@ -72,6 +75,13 @@ export class PreferencesModel {
         // default to the device languages
         this.contentLanguages = deviceLocales.map(locale => locale.languageCode)
       }
+      if (
+        hasProp(v, 'pinnedFeeds') &&
+        Array.isArray(v.pinnedFeeds) &&
+        typeof v.pinnedFeeds.every(item => typeof item === 'string')
+      ) {
+        this.pinnedFeeds = v.pinnedFeeds
+      }
     }
   }
 
@@ -88,9 +98,18 @@ export class PreferencesModel {
           AppBskyActorDefs.isContentLabelPref(pref) &&
           AppBskyActorDefs.validateAdultContentPref(pref).success
         ) {
-          if (LABEL_GROUPS.includes(pref.label)) {
-            this.contentLabels[pref.label] = pref.visibility
+          if (
+            LABEL_GROUPS.includes(pref.label) &&
+            VISIBILITY_VALUES.includes(pref.visibility)
+          ) {
+            this.contentLabels[pref.label as keyof LabelPreferencesModel] =
+              pref.visibility as LabelPreference
           }
+        } else if (
+          AppBskyActorDefs.isPinnedFeedsPref(pref) &&
+          AppBskyActorDefs.validatePinnedFeedsPref(pref).success
+        ) {
+          this.pinnedFeeds = pref.feeds
         }
       }
     })
@@ -200,4 +219,39 @@ export class PreferencesModel {
     }
     return res
   }
+
+  async setPinnedFeeds(v: string[]) {
+    const old = this.pinnedFeeds
+    this.pinnedFeeds = v
+    try {
+      await this.update((prefs: AppBskyActorDefs.Preferences) => {
+        const existing = prefs.find(
+          pref =>
+            AppBskyActorDefs.isPinnedFeedsPref(pref) &&
+            AppBskyActorDefs.validatePinnedFeedsPref(pref).success,
+        )
+        if (existing) {
+          existing.feeds = v
+        } else {
+          prefs.push({
+            $type: 'app.bsky.actor.defs#pinnedFeedsPref',
+            feeds: v,
+          })
+        }
+      })
+    } catch (e) {
+      runInAction(() => {
+        this.pinnedFeeds = old
+      })
+      throw e
+    }
+  }
+
+  async addPinnedFeed(v: string) {
+    return this.setPinnedFeeds([...this.pinnedFeeds, v])
+  }
+
+  async removePinnedFeed(v: string) {
+    return this.setPinnedFeeds(this.pinnedFeeds.filter(uri => uri !== v))
+  }
 }
diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts
index d68664c2d..f500aef2e 100644
--- a/src/state/models/ui/saved-feeds.ts
+++ b/src/state/models/ui/saved-feeds.ts
@@ -1,12 +1,11 @@
 import {makeAutoObservable, runInAction} from 'mobx'
-import {AppBskyFeedGetSavedFeeds as GetSavedFeeds} from '@atproto/api'
+import {AppBskyFeedDefs} from '@atproto/api'
 import {RootStoreModel} from '../root-store'
 import {bundleAsync} from 'lib/async/bundle'
 import {cleanError} from 'lib/strings/errors'
 import {CustomFeedModel} from '../feeds/custom-feed'
-import {hasProp, isObj} from 'lib/type-guards'
 
-const PAGE_SIZE = 30
+const PAGE_SIZE = 100
 
 export class SavedFeedsModel {
   // state
@@ -14,12 +13,9 @@ export class SavedFeedsModel {
   isRefreshing = false
   hasLoaded = false
   error = ''
-  hasMore = true
-  loadMoreCursor?: string
 
   // data
   feeds: CustomFeedModel[] = []
-  pinned: CustomFeedModel[] = []
 
   constructor(public rootStore: RootStoreModel) {
     makeAutoObservable(
@@ -31,24 +27,6 @@ export class SavedFeedsModel {
     )
   }
 
-  serialize() {
-    return {
-      pinned: this.pinned.map(f => f.serialize()),
-    }
-  }
-
-  hydrate(v: unknown) {
-    if (isObj(v)) {
-      if (hasProp(v, 'pinned')) {
-        const pinnedSerialized = (v as any).pinned as string[]
-        const pinnedDeserialized = pinnedSerialized.map(
-          (s: string) => new CustomFeedModel(this.rootStore, JSON.parse(s)),
-        )
-        this.pinned = pinnedDeserialized
-      }
-    }
-  }
-
   get hasContent() {
     return this.feeds.length > 0
   }
@@ -61,149 +39,121 @@ export class SavedFeedsModel {
     return this.hasLoaded && !this.hasContent
   }
 
-  get numFeeds() {
-    return this.feeds.length
+  get pinned() {
+    return this.rootStore.preferences.pinnedFeeds
+      .map(uri => this.feeds.find(f => f.uri === uri) as CustomFeedModel)
+      .filter(Boolean)
   }
 
   get unpinned() {
-    return this.feeds.filter(
-      f => !this.pinned.find(p => p.data.uri === f.data.uri),
-    )
-  }
-
-  get feedNames() {
-    return this.feeds.map(f => f.displayName)
+    return this.feeds.filter(f => !this.isPinned(f))
   }
 
   get pinnedFeedNames() {
     return this.pinned.map(f => f.displayName)
   }
 
-  togglePinnedFeed(feed: CustomFeedModel) {
-    if (!this.isPinned(feed)) {
-      this.pinned = [...this.pinned, feed]
-    } else {
-      this.removePinnedFeed(feed.data.uri)
-    }
-  }
-
-  removePinnedFeed(uri: string) {
-    this.pinned = this.pinned.filter(f => f.data.uri !== uri)
-  }
-
-  reorderPinnedFeeds(temp: CustomFeedModel[]) {
-    this.pinned = temp.filter(item => this.isPinned(item))
-  }
-
-  isPinned(feed: CustomFeedModel) {
-    return this.pinned.find(f => f.data.uri === feed.data.uri) ? true : false
-  }
-
-  movePinnedItem(item: CustomFeedModel, direction: 'up' | 'down') {
-    if (this.pinned.length < 2) {
-      throw new Error('Array must have at least 2 items')
-    }
-    const index = this.pinned.indexOf(item)
-    if (index === -1) {
-      throw new Error('Item not found in array')
-    }
-
-    const len = this.pinned.length
-
-    runInAction(() => {
-      if (direction === 'up') {
-        if (index === 0) {
-          // Remove the item from the first place and put it at the end
-          this.pinned.push(this.pinned.shift()!)
-        } else {
-          // Swap the item with the one before it
-          const temp = this.pinned[index]
-          this.pinned[index] = this.pinned[index - 1]
-          this.pinned[index - 1] = temp
-        }
-      } else if (direction === 'down') {
-        if (index === len - 1) {
-          // Remove the item from the last place and put it at the start
-          this.pinned.unshift(this.pinned.pop()!)
-        } else {
-          // Swap the item with the one after it
-          const temp = this.pinned[index]
-          this.pinned[index] = this.pinned[index + 1]
-          this.pinned[index + 1] = temp
-        }
-      }
-      // this.pinned = [...this.pinned]
-    })
-  }
-
   // public api
   // =
 
-  async refresh(quietRefresh = false) {
-    return this.loadMore(true, quietRefresh)
-  }
-
   clear() {
     this.isLoading = false
     this.isRefreshing = false
     this.hasLoaded = false
     this.error = ''
-    this.hasMore = true
-    this.loadMoreCursor = undefined
     this.feeds = []
   }
 
-  loadMore = bundleAsync(
-    async (replace: boolean = false, quietRefresh = false) => {
-      if (!replace && !this.hasMore) {
-        return
-      }
-      this._xLoading(replace && !quietRefresh)
-      try {
+  refresh = bundleAsync(async (quietRefresh = false) => {
+    this._xLoading(!quietRefresh)
+    try {
+      let feeds: AppBskyFeedDefs.GeneratorView[] = []
+      let cursor
+      for (let i = 0; i < 100; i++) {
         const res = await this.rootStore.agent.app.bsky.feed.getSavedFeeds({
           limit: PAGE_SIZE,
-          cursor: replace ? undefined : this.loadMoreCursor,
+          cursor,
         })
-        if (replace) {
-          this._replaceAll(res)
-        } else {
-          this._appendAll(res)
+        feeds = feeds.concat(res.data.feeds)
+        cursor = res.data.cursor
+        if (!cursor) {
+          break
         }
-        this._xIdle()
-      } catch (e: any) {
-        this._xIdle(e)
       }
-    },
-  )
-
-  removeFeed(uri: string) {
-    this.feeds = this.feeds.filter(f => f.data.uri !== uri)
-  }
-
-  addFeed(algoItem: CustomFeedModel) {
-    this.feeds.push(new CustomFeedModel(this.rootStore, algoItem.data))
-  }
+      runInAction(() => {
+        this.feeds = feeds.map(f => new CustomFeedModel(this.rootStore, f))
+      })
+      this._xIdle()
+    } catch (e: any) {
+      this._xIdle(e)
+    }
+  })
 
-  async save(algoItem: CustomFeedModel) {
+  async save(feed: CustomFeedModel) {
     try {
-      await algoItem.save()
-      this.addFeed(algoItem)
+      await feed.save()
+      runInAction(() => {
+        this.feeds = [
+          ...this.feeds,
+          new CustomFeedModel(this.rootStore, feed.data),
+        ]
+      })
     } catch (e: any) {
       this.rootStore.log.error('Failed to save feed', e)
     }
   }
 
-  async unsave(algoItem: CustomFeedModel) {
-    const uri = algoItem.uri
+  async unsave(feed: CustomFeedModel) {
+    const uri = feed.uri
     try {
-      await algoItem.unsave()
-      this.removeFeed(uri)
-      this.removePinnedFeed(uri)
+      if (this.isPinned(feed)) {
+        await this.rootStore.preferences.removePinnedFeed(uri)
+      }
+      await feed.unsave()
+      runInAction(() => {
+        this.feeds = this.feeds.filter(f => f.data.uri !== uri)
+      })
     } catch (e: any) {
       this.rootStore.log.error('Failed to unsave feed', e)
     }
   }
 
+  async togglePinnedFeed(feed: CustomFeedModel) {
+    if (!this.isPinned(feed)) {
+      return this.rootStore.preferences.addPinnedFeed(feed.uri)
+    } else {
+      return this.rootStore.preferences.removePinnedFeed(feed.uri)
+    }
+  }
+
+  async reorderPinnedFeeds(feeds: CustomFeedModel[]) {
+    return this.rootStore.preferences.setPinnedFeeds(
+      feeds.filter(feed => this.isPinned(feed)).map(feed => feed.uri),
+    )
+  }
+
+  isPinned(feed: CustomFeedModel) {
+    return this.rootStore.preferences.pinnedFeeds.includes(feed.uri)
+  }
+
+  async movePinnedFeed(item: CustomFeedModel, direction: 'up' | 'down') {
+    const pinned = this.rootStore.preferences.pinnedFeeds.slice()
+    const index = pinned.indexOf(item.uri)
+    if (index === -1) {
+      return
+    }
+    if (direction === 'up' && index !== 0) {
+      const temp = pinned[index]
+      pinned[index] = pinned[index - 1]
+      pinned[index - 1] = temp
+    } else if (direction === 'down' && index < pinned.length - 1) {
+      const temp = pinned[index]
+      pinned[index] = pinned[index + 1]
+      pinned[index + 1] = temp
+    }
+    await this.rootStore.preferences.setPinnedFeeds(pinned)
+  }
+
   // state transitions
   // =
 
@@ -219,23 +169,7 @@ export class SavedFeedsModel {
     this.hasLoaded = true
     this.error = cleanError(err)
     if (err) {
-      this.rootStore.log.error('Failed to fetch user followers', err)
-    }
-  }
-
-  // helper functions
-  // =
-
-  _replaceAll(res: GetSavedFeeds.Response) {
-    this.feeds = []
-    this._appendAll(res)
-  }
-
-  _appendAll(res: GetSavedFeeds.Response) {
-    this.loadMoreCursor = res.data.cursor
-    this.hasMore = !!this.loadMoreCursor
-    for (const f of res.data.feeds) {
-      this.feeds.push(new CustomFeedModel(this.rootStore, f))
+      this.rootStore.log.error('Failed to fetch user feeds', err)
     }
   }
 }
diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx
index 911c33da4..d4e843b67 100644
--- a/src/view/com/feeds/CustomFeed.tsx
+++ b/src/view/com/feeds/CustomFeed.tsx
@@ -39,20 +39,30 @@ export const CustomFeed = observer(
     const pal = usePalette('default')
     const navigation = useNavigation<NavigationProp>()
 
-    const onToggleSaved = React.useCallback(() => {
+    const onToggleSaved = React.useCallback(async () => {
       if (item.data.viewer?.saved) {
         store.shell.openModal({
           name: 'confirm',
           title: 'Remove from my feeds',
           message: `Remove ${item.displayName} from my feeds?`,
-          onPressConfirm: () => {
-            store.me.savedFeeds.unsave(item)
-            Toast.show('Removed from my feeds')
+          onPressConfirm: async () => {
+            try {
+              await store.me.savedFeeds.unsave(item)
+              Toast.show('Removed from my feeds')
+            } catch (e) {
+              Toast.show('There was an issue contacting your server')
+              store.log.error('Failed to unsave feed', {e})
+            }
           },
         })
       } else {
-        store.me.savedFeeds.save(item)
-        Toast.show('Added to my feeds')
+        try {
+          await store.me.savedFeeds.save(item)
+          Toast.show('Added to my feeds')
+        } catch (e) {
+          Toast.show('There was an issue contacting your server')
+          store.log.error('Failed to save feed', {e})
+        }
       }
     }, [store, item])
 
diff --git a/src/view/com/feeds/SavedFeeds.tsx b/src/view/com/feeds/SavedFeeds.tsx
index 7135fdf0a..1cb109a43 100644
--- a/src/view/com/feeds/SavedFeeds.tsx
+++ b/src/view/com/feeds/SavedFeeds.tsx
@@ -29,6 +29,10 @@ export const SavedFeeds = observer(
       }
     }, [store, isPageFocused])
 
+    const onRefresh = useCallback(() => {
+      store.me.savedFeeds.refresh()
+    }, [store])
+
     const renderListEmptyComponent = useCallback(() => {
       return (
         <View
@@ -73,7 +77,7 @@ export const SavedFeeds = observer(
         refreshControl={
           <RefreshControl
             refreshing={store.me.savedFeeds.isRefreshing}
-            onRefresh={() => store.me.savedFeeds.refresh()}
+            onRefresh={onRefresh}
             tintColor={pal.colors.text}
             titleColor={pal.colors.text}
             progressViewOffset={headerOffset}
diff --git a/src/view/com/pager/Pager.web.tsx b/src/view/com/pager/Pager.web.tsx
index 107497f6f..7be2b11ec 100644
--- a/src/view/com/pager/Pager.web.tsx
+++ b/src/view/com/pager/Pager.web.tsx
@@ -1,12 +1,9 @@
 import React from 'react'
-import {Animated, View} from 'react-native'
-import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
+import {View} from 'react-native'
 import {s} from 'lib/styles'
 
 export interface RenderTabBarFnProps {
   selectedPage: number
-  position: Animated.Value
-  offset: Animated.Value
   onSelect?: (index: number) => void
 }
 export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
@@ -17,53 +14,51 @@ interface Props {
   renderTabBar: RenderTabBarFn
   onPageSelected?: (index: number) => void
 }
-export const Pager = ({
-  children,
-  tabBarPosition = 'top',
-  initialPage = 0,
-  renderTabBar,
-  onPageSelected,
-}: React.PropsWithChildren<Props>) => {
-  const [selectedPage, setSelectedPage] = React.useState(initialPage)
-  const position = useAnimatedValue(0)
-  const offset = useAnimatedValue(0)
+export const Pager = React.forwardRef(
+  (
+    {
+      children,
+      tabBarPosition = 'top',
+      initialPage = 0,
+      renderTabBar,
+      onPageSelected,
+    }: React.PropsWithChildren<Props>,
+    ref,
+  ) => {
+    const [selectedPage, setSelectedPage] = React.useState(initialPage)
 
-  const onTabBarSelect = React.useCallback(
-    (index: number) => {
-      setSelectedPage(index)
-      onPageSelected?.(index)
-      Animated.timing(position, {
-        toValue: index,
-        duration: 200,
-        useNativeDriver: true,
-      }).start()
-    },
-    [setSelectedPage, onPageSelected, position],
-  )
+    React.useImperativeHandle(ref, () => ({
+      setPage: (index: number) => setSelectedPage(index),
+    }))
 
-  return (
-    <View>
-      {tabBarPosition === 'top' &&
-        renderTabBar({
-          selectedPage,
-          position,
-          offset,
-          onSelect: onTabBarSelect,
-        })}
-      {React.Children.map(children, (child, i) => (
-        <View
-          style={selectedPage === i ? undefined : s.hidden}
-          key={`page-${i}`}>
-          {child}
-        </View>
-      ))}
-      {tabBarPosition === 'bottom' &&
-        renderTabBar({
-          selectedPage,
-          position,
-          offset,
-          onSelect: onTabBarSelect,
-        })}
-    </View>
-  )
-}
+    const onTabBarSelect = React.useCallback(
+      (index: number) => {
+        setSelectedPage(index)
+        onPageSelected?.(index)
+      },
+      [setSelectedPage, onPageSelected],
+    )
+
+    return (
+      <View>
+        {tabBarPosition === 'top' &&
+          renderTabBar({
+            selectedPage,
+            onSelect: onTabBarSelect,
+          })}
+        {React.Children.map(children, (child, i) => (
+          <View
+            style={selectedPage === i ? undefined : s.hidden}
+            key={`page-${i}`}>
+            {child}
+          </View>
+        ))}
+        {tabBarPosition === 'bottom' &&
+          renderTabBar({
+            selectedPage,
+            onSelect: onTabBarSelect,
+          })}
+      </View>
+    )
+  },
+)
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 644182126..54cec3b31 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -4,6 +4,7 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'
 import {AppBskyFeedGetFeed as GetCustomFeed} from '@atproto/api'
 import {observer} from 'mobx-react-lite'
 import useAppState from 'react-native-appstate-hook'
+import isEqual from 'lodash.isequal'
 import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types'
 import {PostsFeedModel} from 'state/models/feeds/posts'
 import {withAuthRequired} from 'view/com/auth/withAuthRequired'
@@ -44,15 +45,26 @@ export const HomeScreen = withAuthRequired(
     }, [store])
 
     React.useEffect(() => {
+      const {pinned} = store.me.savedFeeds
+      if (
+        isEqual(
+          pinned.map(p => p.uri),
+          customFeeds.map(f => (f.params as GetCustomFeed.QueryParams).feed),
+        )
+      ) {
+        // no changes
+        return
+      }
+
       const feeds = []
-      for (const feed of store.me.savedFeeds.pinned) {
+      for (const feed of pinned) {
         const model = new PostsFeedModel(store, 'custom', {feed: feed.uri})
         model.setup()
         feeds.push(model)
       }
       pagerRef.current?.setPage(0)
       setCustomFeeds(feeds)
-    }, [store, store.me.savedFeeds.pinned, setCustomFeeds])
+    }, [store, store.me.savedFeeds.pinned, customFeeds, setCustomFeeds])
 
     React.useEffect(() => {
       // refresh whats hot when lang preferences change
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx
index c2723f694..613e42fbf 100644
--- a/src/view/screens/SavedFeeds.tsx
+++ b/src/view/screens/SavedFeeds.tsx
@@ -27,26 +27,26 @@ import DraggableFlatList, {
 import {CustomFeed} from 'view/com/feeds/CustomFeed'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {CustomFeedModel} from 'state/models/feeds/custom-feed'
+import * as Toast from 'view/com/util/Toast'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'SavedFeeds'>
 
 export const SavedFeeds = withAuthRequired(
   observer(({}: Props) => {
-    // hooks for global items
     const pal = usePalette('default')
-    const rootStore = useStores()
+    const store = useStores()
     const {screen} = useAnalytics()
 
-    // hooks for local
-    const savedFeeds = useMemo(() => rootStore.me.savedFeeds, [rootStore])
+    const savedFeeds = useMemo(() => store.me.savedFeeds, [store])
     useFocusEffect(
       useCallback(() => {
         screen('SavedFeeds')
-        rootStore.shell.setMinimalShellMode(false)
+        store.shell.setMinimalShellMode(false)
         savedFeeds.refresh()
-      }, [screen, rootStore, savedFeeds]),
+      }, [screen, store, savedFeeds]),
     )
-    const _ListEmptyComponent = () => {
+
+    const renderListEmptyComponent = useCallback(() => {
       return (
         <View
           style={[
@@ -56,19 +56,33 @@ export const SavedFeeds = withAuthRequired(
             styles.empty,
           ]}>
           <Text type="lg" style={[pal.text]}>
-            You don't have any pinned feeds. To pin a feed, go back to the Saved
-            Feeds screen and click the pin icon!
+            You don't have any saved feeds.
           </Text>
         </View>
       )
-    }
-    const _ListFooterComponent = () => {
+    }, [pal])
+
+    const renderListFooterComponent = useCallback(() => {
       return (
         <View style={styles.footer}>
           {savedFeeds.isLoading && <ActivityIndicator />}
         </View>
       )
-    }
+    }, [savedFeeds])
+
+    const onRefresh = useCallback(() => savedFeeds.refresh(), [savedFeeds])
+
+    const onDragEnd = useCallback(
+      async ({data}) => {
+        try {
+          await savedFeeds.reorderPinnedFeeds(data)
+        } catch (e) {
+          Toast.show('There was an issue contacting the server')
+          store.log.error('Failed to save pinned feed order', {e})
+        }
+      },
+      [savedFeeds, store],
+    )
 
     return (
       <CenteredView
@@ -90,17 +104,17 @@ export const SavedFeeds = withAuthRequired(
           refreshControl={
             <RefreshControl
               refreshing={savedFeeds.isRefreshing}
-              onRefresh={() => savedFeeds.refresh()}
+              onRefresh={onRefresh}
               tintColor={pal.colors.text}
               titleColor={pal.colors.text}
             />
           }
           renderItem={({item, drag}) => <ListItem item={item} drag={drag} />}
           initialNumToRender={10}
-          ListFooterComponent={_ListFooterComponent}
-          ListEmptyComponent={_ListEmptyComponent}
+          ListFooterComponent={renderListFooterComponent}
+          ListEmptyComponent={renderListEmptyComponent}
           extraData={savedFeeds.isLoading}
-          onDragEnd={({data}) => savedFeeds.reorderPinnedFeeds(data)}
+          onDragEnd={onDragEnd}
         />
       </CenteredView>
     )
@@ -110,13 +124,35 @@ export const SavedFeeds = withAuthRequired(
 const ListItem = observer(
   ({item, drag}: {item: CustomFeedModel; drag: () => void}) => {
     const pal = usePalette('default')
-    const rootStore = useStores()
-    const savedFeeds = useMemo(() => rootStore.me.savedFeeds, [rootStore])
+    const store = useStores()
+    const savedFeeds = useMemo(() => store.me.savedFeeds, [store])
     const isPinned = savedFeeds.isPinned(item)
+
     const onTogglePinned = useCallback(
-      () => savedFeeds.togglePinnedFeed(item),
-      [savedFeeds, item],
+      () =>
+        savedFeeds.togglePinnedFeed(item).catch(e => {
+          Toast.show('There was an issue contacting the server')
+          store.log.error('Failed to toggle pinned feed', {e})
+        }),
+      [savedFeeds, item, store],
+    )
+    const onPressUp = useCallback(
+      () =>
+        savedFeeds.movePinnedFeed(item, 'up').catch(e => {
+          Toast.show('There was an issue contacting the server')
+          store.log.error('Failed to set pinned feed order', {e})
+        }),
+      [store, savedFeeds, item],
     )
+    const onPressDown = useCallback(
+      () =>
+        savedFeeds.movePinnedFeed(item, 'down').catch(e => {
+          Toast.show('There was an issue contacting the server')
+          store.log.error('Failed to set pinned feed order', {e})
+        }),
+      [store, savedFeeds, item],
+    )
+
     return (
       <ScaleDecorator>
         <ShadowDecorator>
@@ -128,9 +164,7 @@ const ListItem = observer(
               <View style={styles.webArrowButtonsContainer}>
                 <TouchableOpacity
                   accessibilityRole="button"
-                  onPress={() => {
-                    savedFeeds.movePinnedItem(item, 'up')
-                  }}>
+                  onPress={onPressUp}>
                   <FontAwesomeIcon
                     icon="arrow-up"
                     size={12}
@@ -139,9 +173,7 @@ const ListItem = observer(
                 </TouchableOpacity>
                 <TouchableOpacity
                   accessibilityRole="button"
-                  onPress={() => {
-                    savedFeeds.movePinnedItem(item, 'down')
-                  }}>
+                  onPress={onPressDown}>
                   <FontAwesomeIcon
                     icon="arrow-down"
                     size={12}
-- 
cgit 1.4.1


From 5537d19e555c39f5f9a0ec16735ea4c3860357c4 Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Thu, 18 May 2023 14:39:04 -0500
Subject: Update saved feeds to use preferences

---
 src/state/models/feeds/custom-feed.ts       | 20 ++--------
 src/state/models/media/image.ts             |  2 +-
 src/state/models/ui/preferences.ts          | 57 ++++++++++++++++++++++-------
 src/state/models/ui/saved-feeds.ts          | 25 +++++++------
 src/state/models/ui/shell.ts                |  2 +-
 src/view/com/feeds/CustomFeed.tsx           |  2 +-
 src/view/com/util/ViewHeader.tsx            |  2 +-
 src/view/com/util/moderation/ImageHider.tsx |  8 ++--
 8 files changed, 68 insertions(+), 50 deletions(-)

(limited to 'src/view/com/feeds/CustomFeed.tsx')

diff --git a/src/state/models/feeds/custom-feed.ts b/src/state/models/feeds/custom-feed.ts
index 5e550ec69..e457d2d1e 100644
--- a/src/state/models/feeds/custom-feed.ts
+++ b/src/state/models/feeds/custom-feed.ts
@@ -38,7 +38,7 @@ export class CustomFeedModel {
   }
 
   get isSaved() {
-    return this.data.viewer?.saved
+    return this.rootStore.preferences.savedFeeds.includes(this.uri)
   }
 
   get isLiked() {
@@ -49,23 +49,11 @@ export class CustomFeedModel {
   // =
 
   async save() {
-    await this.rootStore.agent.app.bsky.feed.saveFeed({
-      feed: this.uri,
-    })
-    runInAction(() => {
-      this.data.viewer = this.data.viewer || {}
-      this.data.viewer.saved = true
-    })
+    await this.rootStore.preferences.addSavedFeed(this.uri)
   }
 
   async unsave() {
-    await this.rootStore.agent.app.bsky.feed.unsaveFeed({
-      feed: this.uri,
-    })
-    runInAction(() => {
-      this.data.viewer = this.data.viewer || {}
-      this.data.viewer.saved = false
-    })
+    await this.rootStore.preferences.removeSavedFeed(this.uri)
   }
 
   async like() {
@@ -82,7 +70,7 @@ export class CustomFeedModel {
   }
 
   async unlike() {
-    if (!this.data.viewer.like) {
+    if (!this.data.viewer?.like) {
       return
     }
     try {
diff --git a/src/state/models/media/image.ts b/src/state/models/media/image.ts
index ec93bf5b6..6edf88d9d 100644
--- a/src/state/models/media/image.ts
+++ b/src/state/models/media/image.ts
@@ -135,7 +135,7 @@ export class ImageModel implements RNImage {
   // Only for mobile
   async crop() {
     try {
-      const cropped = await openCropper({
+      const cropped = await openCropper(this.rootStore, {
         mediaType: 'photo',
         path: this.path,
         freeStyleCropEnabled: true,
diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts
index 05a1eb128..120b4adcc 100644
--- a/src/state/models/ui/preferences.ts
+++ b/src/state/models/ui/preferences.ts
@@ -46,6 +46,7 @@ export class PreferencesModel {
   contentLanguages: string[] =
     deviceLocales?.map?.(locale => locale.languageCode) || []
   contentLabels = new LabelPreferencesModel()
+  savedFeeds: string[] = []
   pinnedFeeds: string[] = []
 
   constructor(public rootStore: RootStoreModel) {
@@ -56,6 +57,7 @@ export class PreferencesModel {
     return {
       contentLanguages: this.contentLanguages,
       contentLabels: this.contentLabels,
+      savedFeeds: this.savedFeeds,
       pinnedFeeds: this.pinnedFeeds,
     }
   }
@@ -75,6 +77,13 @@ export class PreferencesModel {
         // default to the device languages
         this.contentLanguages = deviceLocales.map(locale => locale.languageCode)
       }
+      if (
+        hasProp(v, 'savedFeeds') &&
+        Array.isArray(v.savedFeeds) &&
+        typeof v.savedFeeds.every(item => typeof item === 'string')
+      ) {
+        this.savedFeeds = v.savedFeeds
+      }
       if (
         hasProp(v, 'pinnedFeeds') &&
         Array.isArray(v.pinnedFeeds) &&
@@ -106,10 +115,11 @@ export class PreferencesModel {
               pref.visibility as LabelPreference
           }
         } else if (
-          AppBskyActorDefs.isPinnedFeedsPref(pref) &&
-          AppBskyActorDefs.validatePinnedFeedsPref(pref).success
+          AppBskyActorDefs.isSavedFeedsPref(pref) &&
+          AppBskyActorDefs.validateSavedFeedsPref(pref).success
         ) {
-          this.pinnedFeeds = pref.feeds
+          this.savedFeeds = pref.saved
+          this.pinnedFeeds = pref.pinned
         }
       }
     })
@@ -220,38 +230,57 @@ export class PreferencesModel {
     return res
   }
 
-  async setPinnedFeeds(v: string[]) {
-    const old = this.pinnedFeeds
-    this.pinnedFeeds = v
+  async setSavedFeeds(saved: string[], pinned: string[]) {
+    const oldSaved = this.savedFeeds
+    const oldPinned = this.pinnedFeeds
+    this.savedFeeds = saved
+    this.pinnedFeeds = pinned
     try {
       await this.update((prefs: AppBskyActorDefs.Preferences) => {
         const existing = prefs.find(
           pref =>
-            AppBskyActorDefs.isPinnedFeedsPref(pref) &&
-            AppBskyActorDefs.validatePinnedFeedsPref(pref).success,
+            AppBskyActorDefs.isSavedFeedsPref(pref) &&
+            AppBskyActorDefs.validateSavedFeedsPref(pref).success,
         )
         if (existing) {
-          existing.feeds = v
+          existing.saved = saved
+          existing.pinned = pinned
         } else {
           prefs.push({
-            $type: 'app.bsky.actor.defs#pinnedFeedsPref',
-            feeds: v,
+            $type: 'app.bsky.actor.defs#savedFeedsPref',
+            saved,
+            pinned,
           })
         }
       })
     } catch (e) {
       runInAction(() => {
-        this.pinnedFeeds = old
+        this.savedFeeds = oldSaved
+        this.pinnedFeeds = oldPinned
       })
       throw e
     }
   }
 
+  async addSavedFeed(v: string) {
+    return this.setSavedFeeds([...this.savedFeeds, v], this.pinnedFeeds)
+  }
+
+  async removeSavedFeed(v: string) {
+    return this.setSavedFeeds(
+      this.savedFeeds.filter(uri => uri !== v),
+      this.pinnedFeeds.filter(uri => uri !== v),
+    )
+  }
+
   async addPinnedFeed(v: string) {
-    return this.setPinnedFeeds([...this.pinnedFeeds, v])
+    return this.setSavedFeeds(this.savedFeeds, [...this.pinnedFeeds, v])
   }
 
   async removePinnedFeed(v: string) {
-    return this.setPinnedFeeds(this.pinnedFeeds.filter(uri => uri !== v))
+    return this.setSavedFeeds(
+      this.savedFeeds,
+      this.pinnedFeeds.filter(uri => uri !== v),
+    )
   }
 }
diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts
index f500aef2e..9de28e028 100644
--- a/src/state/models/ui/saved-feeds.ts
+++ b/src/state/models/ui/saved-feeds.ts
@@ -5,8 +5,6 @@ import {bundleAsync} from 'lib/async/bundle'
 import {cleanError} from 'lib/strings/errors'
 import {CustomFeedModel} from '../feeds/custom-feed'
 
-const PAGE_SIZE = 100
-
 export class SavedFeedsModel {
   // state
   isLoading = false
@@ -69,16 +67,15 @@ export class SavedFeedsModel {
     try {
       let feeds: AppBskyFeedDefs.GeneratorView[] = []
       let cursor
-      for (let i = 0; i < 100; i++) {
-        const res = await this.rootStore.agent.app.bsky.feed.getSavedFeeds({
-          limit: PAGE_SIZE,
-          cursor,
+      for (
+        let i = 0;
+        i < this.rootStore.preferences.savedFeeds.length;
+        i += 25
+      ) {
+        const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerators({
+          feeds: this.rootStore.preferences.savedFeeds.slice(i, 25),
         })
         feeds = feeds.concat(res.data.feeds)
-        cursor = res.data.cursor
-        if (!cursor) {
-          break
-        }
       }
       runInAction(() => {
         this.feeds = feeds.map(f => new CustomFeedModel(this.rootStore, f))
@@ -127,7 +124,8 @@ export class SavedFeedsModel {
   }
 
   async reorderPinnedFeeds(feeds: CustomFeedModel[]) {
-    return this.rootStore.preferences.setPinnedFeeds(
+    return this.rootStore.preferences.setSavedFeeds(
+      this.rootStore.preferences.savedFeeds,
       feeds.filter(feed => this.isPinned(feed)).map(feed => feed.uri),
     )
   }
@@ -151,7 +149,10 @@ export class SavedFeedsModel {
       pinned[index] = pinned[index + 1]
       pinned[index + 1] = temp
     }
-    await this.rootStore.preferences.setPinnedFeeds(pinned)
+    await this.rootStore.preferences.setSavedFeeds(
+      this.rootStore.preferences.savedFeeds,
+      pinned,
+    )
   }
 
   // state transitions
diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts
index 9b9a176be..95b666243 100644
--- a/src/state/models/ui/shell.ts
+++ b/src/state/models/ui/shell.ts
@@ -119,7 +119,7 @@ export type Modal =
   // Moderation
   | ReportAccountModal
   | ReportPostModal
-  | CreateMuteListModal
+  | CreateOrEditMuteListModal
   | ListAddRemoveUserModal
 
   // Posts
diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx
index d4e843b67..9a71eb846 100644
--- a/src/view/com/feeds/CustomFeed.tsx
+++ b/src/view/com/feeds/CustomFeed.tsx
@@ -40,7 +40,7 @@ export const CustomFeed = observer(
     const navigation = useNavigation<NavigationProp>()
 
     const onToggleSaved = React.useCallback(async () => {
-      if (item.data.viewer?.saved) {
+      if (item.isSaved) {
         store.shell.openModal({
           name: 'confirm',
           title: 'Remove from my feeds',
diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx
index 7f13f1838..c17a65b14 100644
--- a/src/view/com/util/ViewHeader.tsx
+++ b/src/view/com/util/ViewHeader.tsx
@@ -121,7 +121,7 @@ const Container = observer(
   }: {
     children: React.ReactNode
     hideOnScroll: boolean
-    showBorder: boolean
+    showBorder?: boolean
   }) => {
     const store = useStores()
     const pal = usePalette('default')
diff --git a/src/view/com/util/moderation/ImageHider.tsx b/src/view/com/util/moderation/ImageHider.tsx
index b42c6397d..40add5b67 100644
--- a/src/view/com/util/moderation/ImageHider.tsx
+++ b/src/view/com/util/moderation/ImageHider.tsx
@@ -27,6 +27,10 @@ export function ImageHider({
     setOverride(false)
   }, [setOverride])
 
+  if (moderation.behavior === ModerationBehaviorCode.Hide) {
+    return null
+  }
+
   if (moderation.behavior !== ModerationBehaviorCode.WarnImages) {
     return (
       <View testID={testID} style={style}>
@@ -35,10 +39,6 @@ export function ImageHider({
     )
   }
 
-  if (moderation.behavior === ModerationBehaviorCode.Hide) {
-    return null
-  }
-
   return (
     <View style={[styles.container, containerStyle]}>
       <View testID={testID} style={style}>
-- 
cgit 1.4.1


From 881be748d52f0bbf19543ac8a08d81b46bb8af5d Mon Sep 17 00:00:00 2001
From: Ansh Nanda <anshnanda10@gmail.com>
Date: Thu, 25 May 2023 13:54:43 -0700
Subject: increase save custom feed btn hitslop

---
 src/view/com/feeds/CustomFeed.tsx | 1 +
 1 file changed, 1 insertion(+)

(limited to 'src/view/com/feeds/CustomFeed.tsx')

diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx
index 9a71eb846..748b89c02 100644
--- a/src/view/com/feeds/CustomFeed.tsx
+++ b/src/view/com/feeds/CustomFeed.tsx
@@ -98,6 +98,7 @@ export const CustomFeed = observer(
                 }
                 accessibilityHint=""
                 onPress={onToggleSaved}
+                hitSlop={15}
                 style={styles.btn}>
                 {item.isSaved ? (
                   <FontAwesomeIcon
-- 
cgit 1.4.1

