about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/state/models/feeds/algo/algo-item.ts4
-rw-r--r--src/state/models/feeds/algo/saved.ts42
-rw-r--r--src/state/models/me.ts4
-rw-r--r--src/view/com/algos/AlgoItem.tsx67
-rw-r--r--src/view/index.ts2
-rw-r--r--src/view/screens/CustomFeed.tsx2
-rw-r--r--src/view/screens/SavedFeeds.tsx107
7 files changed, 170 insertions, 58 deletions
diff --git a/src/state/models/feeds/algo/algo-item.ts b/src/state/models/feeds/algo/algo-item.ts
index 0eaeebd39..39bc760ac 100644
--- a/src/state/models/feeds/algo/algo-item.ts
+++ b/src/state/models/feeds/algo/algo-item.ts
@@ -153,4 +153,8 @@ export class AlgoItemModel {
     })
     this.data = res.data.view
   }
+
+  serialize() {
+    return JSON.stringify(this.data)
+  }
 }
diff --git a/src/state/models/feeds/algo/saved.ts b/src/state/models/feeds/algo/saved.ts
index 5d2f854dc..15859fe0c 100644
--- a/src/state/models/feeds/algo/saved.ts
+++ b/src/state/models/feeds/algo/saved.ts
@@ -4,6 +4,7 @@ import {RootStoreModel} from '../../root-store'
 import {bundleAsync} from 'lib/async/bundle'
 import {cleanError} from 'lib/strings/errors'
 import {AlgoItemModel} from './algo-item'
+import {hasProp, isObj} from 'lib/type-guards'
 
 const PAGE_SIZE = 30
 
@@ -18,6 +19,7 @@ export class SavedFeedsModel {
 
   // data
   feeds: AlgoItemModel[] = []
+  pinned: AlgoItemModel[] = []
 
   constructor(public rootStore: RootStoreModel) {
     makeAutoObservable(
@@ -29,6 +31,24 @@ 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 AlgoItemModel(this.rootStore, JSON.parse(s)),
+        )
+        this.pinned = pinnedDeserialized
+      }
+    }
+  }
+
   get hasContent() {
     return this.feeds.length > 0
   }
@@ -51,6 +71,28 @@ export class SavedFeedsModel {
     )
   }
 
+  get savedFeedsWithoutPinned() {
+    return this.feeds.filter(
+      f => !this.pinned.find(p => p.data.uri === f.data.uri),
+    )
+  }
+
+  togglePinnedFeed(feed: AlgoItemModel) {
+    if (!this.isPinned(feed)) {
+      this.pinned.push(feed)
+    } else {
+      this.pinned = this.pinned.filter(f => f.data.uri !== feed.data.uri)
+    }
+  }
+
+  reorderPinnedFeeds(temp: AlgoItemModel[]) {
+    this.pinned = temp
+  }
+
+  isPinned(feed: AlgoItemModel) {
+    return this.pinned.find(f => f.data.uri === feed.data.uri) ? true : false
+  }
+
   // public api
   // =
 
diff --git a/src/state/models/me.ts b/src/state/models/me.ts
index 314e76b9c..68c89ac9b 100644
--- a/src/state/models/me.ts
+++ b/src/state/models/me.ts
@@ -69,6 +69,7 @@ export class MeModel {
       displayName: this.displayName,
       description: this.description,
       avatar: this.avatar,
+      savedFeeds: this.savedFeeds.serialize(),
     }
   }
 
@@ -90,6 +91,9 @@ 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
diff --git a/src/view/com/algos/AlgoItem.tsx b/src/view/com/algos/AlgoItem.tsx
index 4377e3583..f7d320530 100644
--- a/src/view/com/algos/AlgoItem.tsx
+++ b/src/view/com/algos/AlgoItem.tsx
@@ -19,7 +19,17 @@ import {useStores} from 'state/index'
 import {HeartIconSolid} from 'lib/icons'
 
 const AlgoItem = observer(
-  ({item, style}: {item: AlgoItemModel; style?: StyleProp<ViewStyle>}) => {
+  ({
+    item,
+    style,
+    showBottom = true,
+    onLongPress,
+  }: {
+    item: AlgoItemModel
+    style?: StyleProp<ViewStyle>
+    showBottom?: boolean
+    onLongPress?: () => void
+  }) => {
     const store = useStores()
     const pal = usePalette('default')
     const navigation = useNavigation<NavigationProp>()
@@ -34,10 +44,11 @@ const AlgoItem = observer(
             rkey: item.data.uri,
           })
         }}
+        onLongPress={onLongPress}
         key={item.data.uri}>
         <View style={[styles.headerContainer]}>
           <View style={[s.mr10]}>
-            <UserAvatar size={36} avatar={item.data.avatar} s />
+            <UserAvatar size={36} avatar={item.data.avatar} />
           </View>
           <View style={[styles.headerTextContainer]}>
             <Text style={[pal.text, s.bold]}>
@@ -49,37 +60,39 @@ const AlgoItem = observer(
           </View>
         </View>
 
-        <View style={styles.bottomContainer}>
-          <View style={styles.likedByContainer}>
-            {/* <View style={styles.likedByAvatars}>
+        {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 > 1
-                ? `Liked by ${item.data.likeCount} others`
-                : 'Be the first to like this'}
-            </Text>
-          </View>
-          <View>
-            <Button
-              type="inverted"
-              onPress={() => {
-                if (item.data.viewer?.saved) {
-                  item.unsave()
-                  store.me.savedFeeds.removeFeed(item.data.uri)
-                } else {
-                  item.save()
-                  store.me.savedFeeds.addFeed(item)
-                }
-              }}
-              label={item.data.viewer?.saved ? 'Unsave' : 'Save'}
-            />
+              <HeartIconSolid size={16} style={[s.mr2, {color: colors.red3}]} />
+              <Text style={[pal.text, pal.textLight]}>
+                {item.data.likeCount && item.data.likeCount > 1
+                  ? `Liked by ${item.data.likeCount} others`
+                  : 'Be the first to like this'}
+              </Text>
+            </View>
+            <View>
+              <Button
+                type="inverted"
+                onPress={() => {
+                  if (item.data.viewer?.saved) {
+                    item.unsave()
+                    store.me.savedFeeds.removeFeed(item.data.uri)
+                  } else {
+                    item.save()
+                    store.me.savedFeeds.addFeed(item)
+                  }
+                }}
+                label={item.data.viewer?.saved ? 'Unsave' : 'Save'}
+              />
+            </View>
           </View>
-        </View>
+        ) : null}
       </TouchableOpacity>
     )
   },
diff --git a/src/view/index.ts b/src/view/index.ts
index dd8a585d6..253735e81 100644
--- a/src/view/index.ts
+++ b/src/view/index.ts
@@ -75,6 +75,7 @@ import {faX} from '@fortawesome/free-solid-svg-icons/faX'
 import {faXmark} from '@fortawesome/free-solid-svg-icons/faXmark'
 import {faPlay} from '@fortawesome/free-solid-svg-icons/faPlay'
 import {faPause} from '@fortawesome/free-solid-svg-icons/faPause'
+import {faThumbtack} from '@fortawesome/free-solid-svg-icons/faThumbtack'
 
 export function setup() {
   library.add(
@@ -149,6 +150,7 @@ export function setup() {
     faUserXmark,
     faTicket,
     faTrashCan,
+    faThumbtack,
     faX,
     faXmark,
     faPlay,
diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx
index 05c8d38f1..97dd1cf81 100644
--- a/src/view/screens/CustomFeed.tsx
+++ b/src/view/screens/CustomFeed.tsx
@@ -19,7 +19,7 @@ import {Text} from 'view/com/util/text/Text'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'CustomFeed'>
 export const CustomFeed = withAuthRequired(
-  observer(({route, navigation}: Props) => {
+  observer(({route}: Props) => {
     const rootStore = useStores()
     const {rkey, name} = route.params
     const currentFeed = useCustomFeed(rkey)
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx
index 7b04a6474..65ffdb233 100644
--- a/src/view/screens/SavedFeeds.tsx
+++ b/src/view/screens/SavedFeeds.tsx
@@ -3,8 +3,9 @@ import {
   RefreshControl,
   StyleSheet,
   View,
-  FlatList,
   ActivityIndicator,
+  FlatList,
+  TouchableOpacity,
 } from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
@@ -13,28 +14,28 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {CommonNavigatorParams} from 'lib/routes/types'
 import {observer} from 'mobx-react-lite'
 import {useStores} from 'state/index'
-import {SavedFeedsModel} from 'state/models/feeds/algo/saved'
 import AlgoItem from 'view/com/algos/AlgoItem'
 import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {ViewHeader} from 'view/com/util/ViewHeader'
 import {CenteredView} from 'view/com/util/Views'
 import {Text} from 'view/com/util/text/Text'
 import {isDesktopWeb} from 'platform/detection'
-import {s} from 'lib/styles'
+import {colors, s} from 'lib/styles'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {AlgoItemModel} from 'state/models/feeds/algo/algo-item'
+import {SavedFeedsModel} from 'state/models/feeds/algo/saved'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'SavedFeeds'>
 
 export const SavedFeeds = withAuthRequired(
   observer(({}: Props) => {
+    // hooks for global items
     const pal = usePalette('default')
     const rootStore = useStores()
     const {screen} = useAnalytics()
 
-    const savedFeeds = useMemo(
-      () => new SavedFeedsModel(rootStore),
-      [rootStore],
-    )
-
+    // hooks for local
+    const savedFeeds = useMemo(() => rootStore.me.savedFeeds, [rootStore])
     useFocusEffect(
       useCallback(() => {
         screen('SavedFeeds')
@@ -42,14 +43,38 @@ export const SavedFeeds = withAuthRequired(
         savedFeeds.refresh()
       }, [screen, rootStore, savedFeeds]),
     )
+    const _ListEmptyComponent = () => {
+      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. To save a feed, click the save
+            button when a custom feed or algorithm shows up.
+          </Text>
+        </View>
+      )
+    }
+    const _ListFooterComponent = () => {
+      return (
+        <View style={styles.footer}>
+          {savedFeeds.isLoading && <ActivityIndicator />}
+        </View>
+      )
+    }
 
     return (
       <CenteredView style={[s.flex1]}>
-        <ViewHeader title="Custom Algorithms" showOnDesktop />
+        <ViewHeader title="Saved Feeds" showOnDesktop />
         <FlatList
           style={[!isDesktopWeb && s.flex1]}
           data={savedFeeds.feeds}
           keyExtractor={item => item.data.uri}
+          refreshing={savedFeeds.isRefreshing}
           refreshControl={
             <RefreshControl
               refreshing={savedFeeds.isRefreshing}
@@ -58,28 +83,12 @@ export const SavedFeeds = withAuthRequired(
               titleColor={pal.colors.text}
             />
           }
-          onEndReached={() => savedFeeds.loadMore()}
-          renderItem={({item}) => <AlgoItem key={item.data.uri} item={item} />}
-          initialNumToRender={15}
-          ListFooterComponent={() => (
-            <View style={styles.footer}>
-              {savedFeeds.isLoading && <ActivityIndicator />}
-            </View>
-          )}
-          ListEmptyComponent={() => (
-            <View
-              style={[
-                pal.border,
-                !isDesktopWeb && s.flex1,
-                pal.viewLight,
-                styles.empty,
-              ]}>
-              <Text type="lg" style={[pal.text]}>
-                You don't have any saved feeds. To save a feed, click the save
-                button when a custom feed or algorithm shows up.
-              </Text>
-            </View>
+          renderItem={({item}) => (
+            <SavedFeedItem item={item} savedFeeds={savedFeeds} />
           )}
+          initialNumToRender={10}
+          ListFooterComponent={_ListFooterComponent}
+          ListEmptyComponent={_ListEmptyComponent}
           extraData={savedFeeds.isLoading}
           // @ts-ignore our .web version only -prf
           desktopFixedHeight
@@ -89,6 +98,36 @@ export const SavedFeeds = withAuthRequired(
   }),
 )
 
+const SavedFeedItem = observer(
+  ({item, savedFeeds}: {item: AlgoItemModel; savedFeeds: SavedFeedsModel}) => {
+    const isPinned = savedFeeds.isPinned(item)
+
+    return (
+      <View style={styles.itemContainer}>
+        <AlgoItem
+          key={item.data.uri}
+          item={item}
+          showBottom={false}
+          style={styles.item}
+        />
+        <TouchableOpacity
+          accessibilityRole="button"
+          onPress={() => {
+            savedFeeds.togglePinnedFeed(item)
+            console.log('pinned', savedFeeds.pinned)
+            console.log('isPinned', savedFeeds.isPinned(item))
+          }}>
+          <FontAwesomeIcon
+            icon="thumb-tack"
+            size={20}
+            color={isPinned ? colors.blue3 : colors.gray3}
+          />
+        </TouchableOpacity>
+      </View>
+    )
+  },
+)
+
 const styles = StyleSheet.create({
   footer: {
     paddingVertical: 20,
@@ -100,4 +139,12 @@ const styles = StyleSheet.create({
     marginHorizontal: 24,
     marginTop: 10,
   },
+  itemContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    marginRight: 18,
+  },
+  item: {
+    borderTopWidth: 0,
+  },
 })