about summary refs log tree commit diff
path: root/src/view/screens/SavedFeeds.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/screens/SavedFeeds.tsx')
-rw-r--r--src/view/screens/SavedFeeds.tsx261
1 files changed, 169 insertions, 92 deletions
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx
index 0003dbd5d..d50f9f74d 100644
--- a/src/view/screens/SavedFeeds.tsx
+++ b/src/view/screens/SavedFeeds.tsx
@@ -1,5 +1,6 @@
 import React from 'react'
 import {ActivityIndicator, Pressable, StyleSheet, View} from 'react-native'
+import {AppBskyActorDefs} from '@atproto/api'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
@@ -9,11 +10,11 @@ import {NativeStackScreenProps} from '@react-navigation/native-stack'
 import {track} from '#/lib/analytics/analytics'
 import {logger} from '#/logger'
 import {
-  usePinFeedMutation,
+  useOverwriteSavedFeedsMutation,
   usePreferencesQuery,
-  useSetSaveFeedsMutation,
-  useUnpinFeedMutation,
+  useUpdateSavedFeedsMutation,
 } from '#/state/queries/preferences'
+import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
 import {useSetMinimalShellMode} from '#/state/shell'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {useHaptics} from 'lib/haptics'
@@ -27,6 +28,10 @@ import {Text} from 'view/com/util/text/Text'
 import * as Toast from 'view/com/util/Toast'
 import {ViewHeader} from 'view/com/util/ViewHeader'
 import {CenteredView, ScrollView} from 'view/com/util/Views'
+import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed'
+import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType'
+import {atoms as a, useTheme} from '#/alf'
+import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline'
 
 const HITSLOP_TOP = {
   top: 20,
@@ -50,23 +55,25 @@ export function SavedFeeds({}: Props) {
   const setMinimalShellMode = useSetMinimalShellMode()
   const {data: preferences} = usePreferencesQuery()
   const {
-    mutateAsync: setSavedFeeds,
+    mutateAsync: overwriteSavedFeeds,
     variables: optimisticSavedFeedsResponse,
     reset: resetSaveFeedsMutationState,
-    error: setSavedFeedsError,
-  } = useSetSaveFeedsMutation()
+    error: savedFeedsError,
+  } = useOverwriteSavedFeedsMutation()
 
   /*
    * Use optimistic data if exists and no error, otherwise fallback to remote
    * data
    */
   const currentFeeds =
-    optimisticSavedFeedsResponse && !setSavedFeedsError
+    optimisticSavedFeedsResponse && !savedFeedsError
       ? optimisticSavedFeedsResponse
-      : preferences?.feeds || {saved: [], pinned: []}
-  const unpinned = currentFeeds.saved.filter(f => {
-    return !currentFeeds.pinned?.includes(f)
-  })
+      : preferences?.savedFeeds || []
+  const pinnedFeeds = currentFeeds.filter(f => f.pinned)
+  const unpinnedFeeds = currentFeeds.filter(f => !f.pinned)
+  const noSavedFeedsOfAnyType = pinnedFeeds.length + unpinnedFeeds.length === 0
+  const noFollowingFeed =
+    currentFeeds.every(f => f.type !== 'timeline') && !noSavedFeedsOfAnyType
 
   useFocusEffect(
     React.useCallback(() => {
@@ -84,14 +91,20 @@ export function SavedFeeds({}: Props) {
       ]}>
       <ViewHeader title={_(msg`Edit My Feeds`)} showOnDesktop showBorder />
       <ScrollView style={s.flex1} contentContainerStyle={[styles.noBorder]}>
+        {noSavedFeedsOfAnyType && (
+          <View style={[pal.border, {borderBottomWidth: 1}]}>
+            <NoSavedFeedsOfAnyType />
+          </View>
+        )}
+
         <View style={[pal.text, pal.border, styles.title]}>
           <Text type="title" style={pal.text}>
             <Trans>Pinned Feeds</Trans>
           </Text>
         </View>
 
-        {preferences?.feeds ? (
-          !currentFeeds.pinned.length ? (
+        {preferences ? (
+          !pinnedFeeds.length ? (
             <View
               style={[
                 pal.border,
@@ -104,27 +117,35 @@ export function SavedFeeds({}: Props) {
               </Text>
             </View>
           ) : (
-            currentFeeds.pinned.map(uri => (
+            pinnedFeeds.map(f => (
               <ListItem
-                key={uri}
-                feedUri={uri}
+                key={f.id}
+                feed={f}
                 isPinned
-                setSavedFeeds={setSavedFeeds}
+                overwriteSavedFeeds={overwriteSavedFeeds}
                 resetSaveFeedsMutationState={resetSaveFeedsMutationState}
                 currentFeeds={currentFeeds}
+                preferences={preferences}
               />
             ))
           )
         ) : (
           <ActivityIndicator style={{marginTop: 20}} />
         )}
+
+        {noFollowingFeed && (
+          <View style={[pal.border, {borderBottomWidth: 1}]}>
+            <NoFollowingFeed />
+          </View>
+        )}
+
         <View style={[pal.text, pal.border, styles.title]}>
           <Text type="title" style={pal.text}>
             <Trans>Saved Feeds</Trans>
           </Text>
         </View>
-        {preferences?.feeds ? (
-          !unpinned.length ? (
+        {preferences ? (
+          !unpinnedFeeds.length ? (
             <View
               style={[
                 pal.border,
@@ -137,14 +158,15 @@ export function SavedFeeds({}: Props) {
               </Text>
             </View>
           ) : (
-            unpinned.map(uri => (
+            unpinnedFeeds.map(f => (
               <ListItem
-                key={uri}
-                feedUri={uri}
+                key={f.id}
+                feed={f}
                 isPinned={false}
-                setSavedFeeds={setSavedFeeds}
+                overwriteSavedFeeds={overwriteSavedFeeds}
                 resetSaveFeedsMutationState={resetSaveFeedsMutationState}
                 currentFeeds={currentFeeds}
+                preferences={preferences}
               />
             ))
           )
@@ -174,27 +196,29 @@ export function SavedFeeds({}: Props) {
 }
 
 function ListItem({
-  feedUri,
+  feed,
   isPinned,
   currentFeeds,
-  setSavedFeeds,
+  overwriteSavedFeeds,
   resetSaveFeedsMutationState,
 }: {
-  feedUri: string // uri
+  feed: AppBskyActorDefs.SavedFeed
   isPinned: boolean
-  currentFeeds: {saved: string[]; pinned: string[]}
-  setSavedFeeds: ReturnType<typeof useSetSaveFeedsMutation>['mutateAsync']
+  currentFeeds: AppBskyActorDefs.SavedFeed[]
+  overwriteSavedFeeds: ReturnType<
+    typeof useOverwriteSavedFeedsMutation
+  >['mutateAsync']
   resetSaveFeedsMutationState: ReturnType<
-    typeof useSetSaveFeedsMutation
+    typeof useOverwriteSavedFeedsMutation
   >['reset']
+  preferences: UsePreferencesQueryResponse
 }) {
   const pal = usePalette('default')
   const {_} = useLingui()
   const playHaptic = useHaptics()
-  const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation()
-  const {isPending: isUnpinPending, mutateAsync: unpinFeed} =
-    useUnpinFeedMutation()
-  const isPending = isPinPending || isUnpinPending
+  const {isPending: isUpdatePending, mutateAsync: updateSavedFeeds} =
+    useUpdateSavedFeedsMutation()
+  const feedUri = feed.value
 
   const onTogglePinned = React.useCallback(async () => {
     playHaptic()
@@ -202,81 +226,82 @@ function ListItem({
     try {
       resetSaveFeedsMutationState()
 
-      if (isPinned) {
-        await unpinFeed({uri: feedUri})
-      } else {
-        await pinFeed({uri: feedUri})
-      }
+      await updateSavedFeeds([
+        {
+          ...feed,
+          pinned: !feed.pinned,
+        },
+      ])
     } catch (e) {
       Toast.show(_(msg`There was an issue contacting the server`))
       logger.error('Failed to toggle pinned feed', {message: e})
     }
-  }, [
-    playHaptic,
-    resetSaveFeedsMutationState,
-    isPinned,
-    unpinFeed,
-    feedUri,
-    pinFeed,
-    _,
-  ])
+  }, [_, playHaptic, feed, updateSavedFeeds, resetSaveFeedsMutationState])
 
   const onPressUp = React.useCallback(async () => {
     if (!isPinned) return
 
-    // create new array, do not mutate
-    const pinned = [...currentFeeds.pinned]
-    const index = pinned.indexOf(feedUri)
+    const nextFeeds = currentFeeds.slice()
+    const ids = currentFeeds.map(f => f.id)
+    const index = ids.indexOf(feed.id)
+    const nextIndex = index - 1
 
     if (index === -1 || index === 0) return
-    ;[pinned[index], pinned[index - 1]] = [pinned[index - 1], pinned[index]]
+    ;[nextFeeds[index], nextFeeds[nextIndex]] = [
+      nextFeeds[nextIndex],
+      nextFeeds[index],
+    ]
 
     try {
-      await setSavedFeeds({saved: currentFeeds.saved, pinned})
+      await overwriteSavedFeeds(nextFeeds)
       track('CustomFeed:Reorder', {
-        uri: feedUri,
-        index: pinned.indexOf(feedUri),
+        uri: feed.value,
+        index: nextIndex,
       })
     } catch (e) {
       Toast.show(_(msg`There was an issue contacting the server`))
       logger.error('Failed to set pinned feed order', {message: e})
     }
-  }, [feedUri, isPinned, setSavedFeeds, currentFeeds, _])
+  }, [feed, isPinned, overwriteSavedFeeds, currentFeeds, _])
 
   const onPressDown = React.useCallback(async () => {
     if (!isPinned) return
 
-    const pinned = [...currentFeeds.pinned]
-    const index = pinned.indexOf(feedUri)
+    const nextFeeds = currentFeeds.slice()
+    const ids = currentFeeds.map(f => f.id)
+    const index = ids.indexOf(feed.id)
+    const nextIndex = index + 1
 
-    if (index === -1 || index >= pinned.length - 1) return
-    ;[pinned[index], pinned[index + 1]] = [pinned[index + 1], pinned[index]]
+    if (index === -1 || index >= nextFeeds.length - 1) return
+    ;[nextFeeds[index], nextFeeds[nextIndex]] = [
+      nextFeeds[nextIndex],
+      nextFeeds[index],
+    ]
 
     try {
-      await setSavedFeeds({saved: currentFeeds.saved, pinned})
+      await overwriteSavedFeeds(nextFeeds)
       track('CustomFeed:Reorder', {
-        uri: feedUri,
-        index: pinned.indexOf(feedUri),
+        uri: feed.value,
+        index: nextIndex,
       })
     } catch (e) {
       Toast.show(_(msg`There was an issue contacting the server`))
       logger.error('Failed to set pinned feed order', {message: e})
     }
-  }, [feedUri, isPinned, setSavedFeeds, currentFeeds, _])
+  }, [feed, isPinned, overwriteSavedFeeds, currentFeeds, _])
 
   return (
-    <Pressable
-      accessibilityRole="button"
-      style={[styles.itemContainer, pal.border]}>
+    <View style={[styles.itemContainer, pal.border]}>
       {isPinned ? (
         <View style={styles.webArrowButtonsContainer}>
           <Pressable
-            disabled={isPending}
+            disabled={isUpdatePending}
             accessibilityRole="button"
             onPress={onPressUp}
             hitSlop={HITSLOP_TOP}
             style={state => ({
-              opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
+              opacity:
+                state.hovered || state.focused || isUpdatePending ? 0.5 : 1,
             })}>
             <FontAwesomeIcon
               icon="arrow-up"
@@ -285,39 +310,92 @@ function ListItem({
             />
           </Pressable>
           <Pressable
-            disabled={isPending}
+            disabled={isUpdatePending}
             accessibilityRole="button"
             onPress={onPressDown}
             hitSlop={HITSLOP_BOTTOM}
             style={state => ({
-              opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
+              opacity:
+                state.hovered || state.focused || isUpdatePending ? 0.5 : 1,
             })}>
             <FontAwesomeIcon icon="arrow-down" size={12} style={[pal.text]} />
           </Pressable>
         </View>
       ) : null}
-      <FeedSourceCard
-        key={feedUri}
-        feedUri={feedUri}
-        style={styles.noTopBorder}
-        showSaveBtn
-        showMinimalPlaceholder
-      />
-      <Pressable
-        disabled={isPending}
-        accessibilityRole="button"
-        hitSlop={10}
-        onPress={onTogglePinned}
-        style={state => ({
-          opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
-        })}>
-        <FontAwesomeIcon
-          icon="thumb-tack"
-          size={20}
-          color={isPinned ? colors.blue3 : pal.colors.icon}
+      {feed.type === 'timeline' ? (
+        <FollowingFeedCard />
+      ) : (
+        <FeedSourceCard
+          key={feedUri}
+          feedUri={feedUri}
+          style={styles.noTopBorder}
+          showSaveBtn
+          showMinimalPlaceholder
+        />
+      )}
+      <View style={{paddingRight: 16}}>
+        <Pressable
+          disabled={isUpdatePending}
+          accessibilityRole="button"
+          hitSlop={10}
+          onPress={onTogglePinned}
+          style={state => ({
+            opacity:
+              state.hovered || state.focused || isUpdatePending ? 0.5 : 1,
+          })}>
+          <FontAwesomeIcon
+            icon="thumb-tack"
+            size={20}
+            color={isPinned ? colors.blue3 : pal.colors.icon}
+          />
+        </Pressable>
+      </View>
+    </View>
+  )
+}
+
+function FollowingFeedCard() {
+  const t = useTheme()
+  return (
+    <View
+      style={[
+        a.flex_row,
+        a.align_center,
+        a.flex_1,
+        {
+          paddingHorizontal: 18,
+          paddingVertical: 20,
+        },
+      ]}>
+      <View
+        style={[
+          a.align_center,
+          a.justify_center,
+          a.rounded_sm,
+          {
+            width: 36,
+            height: 36,
+            backgroundColor: t.palette.primary_500,
+            marginRight: 10,
+          },
+        ]}>
+        <FilterTimeline
+          style={[
+            {
+              width: 22,
+              height: 22,
+            },
+          ]}
+          fill={t.palette.white}
         />
-      </Pressable>
-    </Pressable>
+      </View>
+      <View
+        style={{flex: 1, flexDirection: 'row', gap: 8, alignItems: 'center'}}>
+        <Text type="lg-medium" style={[t.atoms.text]} numberOfLines={1}>
+          <Trans>Following</Trans>
+        </Text>
+      </View>
+    </View>
   )
 }
 
@@ -345,7 +423,6 @@ const styles = StyleSheet.create({
     flexDirection: 'row',
     alignItems: 'center',
     borderBottomWidth: 1,
-    paddingRight: 16,
   },
   webArrowButtonsContainer: {
     paddingLeft: 16,