about summary refs log tree commit diff
path: root/src/view/screens/Feeds.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/screens/Feeds.tsx')
-rw-r--r--src/view/screens/Feeds.tsx320
1 files changed, 245 insertions, 75 deletions
diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx
index 97c6e8672..d2c4a6d2d 100644
--- a/src/view/screens/Feeds.tsx
+++ b/src/view/screens/Feeds.tsx
@@ -1,90 +1,72 @@
 import React from 'react'
-import {StyleSheet, View} from 'react-native'
-import {useFocusEffect} from '@react-navigation/native'
-import isEqual from 'lodash.isequal'
+import {ActivityIndicator, StyleSheet, RefreshControl, View} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
+import {AtUri} from '@atproto/api'
 import {withAuthRequired} from 'view/com/auth/withAuthRequired'
-import {FlatList} from 'view/com/util/Views'
 import {ViewHeader} from 'view/com/util/ViewHeader'
-import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
 import {FAB} from 'view/com/util/fab/FAB'
 import {Link} from 'view/com/util/Link'
 import {NativeStackScreenProps, FeedsTabNavigatorParams} from 'lib/routes/types'
 import {observer} from 'mobx-react-lite'
-import {PostsMultiFeedModel} from 'state/models/feeds/multi-feed'
-import {MultiFeed} from 'view/com/posts/MultiFeed'
 import {usePalette} from 'lib/hooks/usePalette'
-import {useTimer} from 'lib/hooks/useTimer'
 import {useStores} from 'state/index'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
 import {ComposeIcon2, CogIcon} from 'lib/icons'
 import {s} from 'lib/styles'
-
-const LOAD_NEW_PROMPT_TIME = 60e3 // 60 seconds
-const MOBILE_HEADER_OFFSET = 40
+import {SearchInput} from 'view/com/util/forms/SearchInput'
+import {UserAvatar} from 'view/com/util/UserAvatar'
+import {FeedFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
+import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
+import debounce from 'lodash.debounce'
+import {Text} from 'view/com/util/text/Text'
+import {MyFeedsUIModel, MyFeedsItem} from 'state/models/ui/my-feeds'
+import {FlatList} from 'view/com/util/Views'
+import {useFocusEffect} from '@react-navigation/native'
+import {CustomFeed} from 'view/com/feeds/CustomFeed'
 
 type Props = NativeStackScreenProps<FeedsTabNavigatorParams, 'Feeds'>
 export const FeedsScreen = withAuthRequired(
   observer<Props>(function FeedsScreenImpl({}: Props) {
     const pal = usePalette('default')
     const store = useStores()
-    const {isMobile} = useWebMediaQueries()
-    const flatListRef = React.useRef<FlatList>(null)
-    const multifeed = React.useMemo<PostsMultiFeedModel>(
-      () => new PostsMultiFeedModel(store),
-      [store],
+    const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
+    const myFeeds = React.useMemo(() => new MyFeedsUIModel(store), [store])
+    const [query, setQuery] = React.useState<string>('')
+    const debouncedSearchFeeds = React.useMemo(
+      () => debounce(q => myFeeds.discovery.search(q), 500), // debounce for 500ms
+      [myFeeds],
     )
-    const [onMainScroll, isScrolledDown, resetMainScroll] =
-      useOnMainScroll(store)
-    const [loadPromptVisible, setLoadPromptVisible] = React.useState(false)
-    const [resetPromptTimer] = useTimer(LOAD_NEW_PROMPT_TIME, () => {
-      setLoadPromptVisible(true)
-    })
-
-    const onSoftReset = React.useCallback(() => {
-      flatListRef.current?.scrollToOffset({offset: 0})
-      multifeed.loadLatest()
-      resetPromptTimer()
-      setLoadPromptVisible(false)
-      resetMainScroll()
-    }, [
-      flatListRef,
-      resetMainScroll,
-      multifeed,
-      resetPromptTimer,
-      setLoadPromptVisible,
-    ])
 
     useFocusEffect(
       React.useCallback(() => {
-        const softResetSub = store.onScreenSoftReset(onSoftReset)
-        const multifeedCleanup = multifeed.registerListeners()
-        const cleanup = () => {
-          softResetSub.remove()
-          multifeedCleanup()
-        }
-
         store.shell.setMinimalShellMode(false)
-        return cleanup
-      }, [store, multifeed, onSoftReset]),
+        myFeeds.setup()
+      }, [store.shell, myFeeds]),
     )
 
-    React.useEffect(() => {
-      if (
-        isEqual(
-          multifeed.feedInfos.map(f => f.uri),
-          store.me.savedFeeds.all.map(f => f.uri),
-        )
-      ) {
-        // no changes
-        return
-      }
-      multifeed.refresh()
-    }, [multifeed, store.me.savedFeeds.all])
-
     const onPressCompose = React.useCallback(() => {
       store.shell.openComposer({})
     }, [store])
+    const onChangeQuery = React.useCallback(
+      (text: string) => {
+        setQuery(text)
+        if (text.length > 1) {
+          debouncedSearchFeeds(text)
+        } else {
+          myFeeds.discovery.refresh()
+        }
+      },
+      [debouncedSearchFeeds, myFeeds.discovery],
+    )
+    const onPressCancelSearch = React.useCallback(() => {
+      setQuery('')
+      myFeeds.discovery.refresh()
+    }, [myFeeds])
+    const onSubmitQuery = React.useCallback(() => {
+      debouncedSearchFeeds(query)
+      debouncedSearchFeeds.flush()
+    }, [debouncedSearchFeeds, query])
 
     const renderHeaderBtn = React.useCallback(() => {
       return (
@@ -99,30 +81,150 @@ export const FeedsScreen = withAuthRequired(
       )
     }, [pal])
 
+    const onRefresh = React.useCallback(() => {
+      myFeeds.refresh()
+    }, [myFeeds])
+
+    const renderItem = React.useCallback(
+      ({item}: {item: MyFeedsItem}) => {
+        if (item.type === 'discover-feeds-loading') {
+          return <FeedFeedLoadingPlaceholder />
+        } else if (item.type === 'spinner') {
+          return (
+            <View style={s.p10}>
+              <ActivityIndicator />
+            </View>
+          )
+        } else if (item.type === 'error') {
+          return <ErrorMessage message={item.error} />
+        } else if (item.type === 'saved-feeds-header') {
+          if (!isMobile) {
+            return (
+              <View
+                style={[
+                  pal.view,
+                  styles.header,
+                  pal.border,
+                  {
+                    borderBottomWidth: 1,
+                  },
+                ]}>
+                <Text type="title-lg" style={[pal.text, s.bold]}>
+                  My Feeds
+                </Text>
+                <Link href="/settings/saved-feeds">
+                  <CogIcon strokeWidth={1.5} style={pal.icon} size={28} />
+                </Link>
+              </View>
+            )
+          }
+          return <View />
+        } else if (item.type === 'saved-feed') {
+          return (
+            <SavedFeed
+              uri={item.feed.uri}
+              avatar={item.feed.data.avatar}
+              displayName={item.feed.displayName}
+            />
+          )
+        } else if (item.type === 'discover-feeds-header') {
+          return (
+            <>
+              <View
+                style={[
+                  pal.view,
+                  styles.header,
+                  {
+                    marginTop: 16,
+                    paddingLeft: isMobile ? 12 : undefined,
+                    paddingRight: 10,
+                    paddingBottom: isMobile ? 6 : undefined,
+                  },
+                ]}>
+                <Text type="title-lg" style={[pal.text, s.bold]}>
+                  Discover new feeds
+                </Text>
+                {!isMobile && (
+                  <SearchInput
+                    query={query}
+                    onChangeQuery={onChangeQuery}
+                    onPressCancelSearch={onPressCancelSearch}
+                    onSubmitQuery={onSubmitQuery}
+                    style={{flex: 1, maxWidth: 250}}
+                  />
+                )}
+              </View>
+              {isMobile && (
+                <View style={{paddingHorizontal: 8, paddingBottom: 10}}>
+                  <SearchInput
+                    query={query}
+                    onChangeQuery={onChangeQuery}
+                    onPressCancelSearch={onPressCancelSearch}
+                    onSubmitQuery={onSubmitQuery}
+                  />
+                </View>
+              )}
+            </>
+          )
+        } else if (item.type === 'discover-feed') {
+          return (
+            <CustomFeed
+              item={item.feed}
+              showSaveBtn
+              showDescription
+              showLikes
+            />
+          )
+        } else if (item.type === 'discover-feeds-no-results') {
+          return (
+            <View
+              style={{
+                paddingHorizontal: 16,
+                paddingTop: 10,
+                paddingBottom: '150%',
+              }}>
+              <Text type="lg" style={pal.textLight}>
+                No results found for "{query}"
+              </Text>
+            </View>
+          )
+        }
+        return null
+      },
+      [isMobile, pal, query, onChangeQuery, onPressCancelSearch, onSubmitQuery],
+    )
+
     return (
       <View style={[pal.view, styles.container]}>
-        <MultiFeed
-          scrollElRef={flatListRef}
-          multifeed={multifeed}
-          onScroll={onMainScroll}
-          scrollEventThrottle={100}
-          headerOffset={isMobile ? MOBILE_HEADER_OFFSET : undefined}
-        />
         {isMobile && (
           <ViewHeader
-            title="My Feeds"
+            title="Feeds"
             canGoBack={false}
-            hideOnScroll
             renderButton={renderHeaderBtn}
+            showBorder
           />
         )}
-        {isScrolledDown || loadPromptVisible ? (
-          <LoadLatestBtn
-            onPress={onSoftReset}
-            label="Load latest posts"
-            showIndicator={loadPromptVisible}
-          />
-        ) : null}
+
+        <FlatList
+          style={[!isTabletOrDesktop && s.flex1, styles.list]}
+          data={myFeeds.items}
+          keyExtractor={item => item._reactKey}
+          contentContainerStyle={styles.contentContainer}
+          refreshControl={
+            <RefreshControl
+              refreshing={myFeeds.isRefreshing}
+              onRefresh={onRefresh}
+              tintColor={pal.colors.text}
+              titleColor={pal.colors.text}
+            />
+          }
+          renderItem={renderItem}
+          initialNumToRender={10}
+          onEndReached={() => myFeeds.loadMore()}
+          extraData={myFeeds.isLoading}
+          // @ts-ignore our .web version only -prf
+          desktopFixedHeight
+        />
         <FAB
           testID="composeFAB"
           onPress={onPressCompose}
@@ -136,8 +238,76 @@ export const FeedsScreen = withAuthRequired(
   }),
 )
 
+function SavedFeed({
+  uri,
+  avatar,
+  displayName,
+}: {
+  uri: string
+  avatar: string | undefined
+  displayName: string
+}) {
+  const pal = usePalette('default')
+  const urip = new AtUri(uri)
+  const href = `/profile/${urip.hostname}/feed/${urip.rkey}`
+  const {isMobile} = useWebMediaQueries()
+  return (
+    <Link
+      testID={`saved-feed-${displayName}`}
+      href={href}
+      style={[pal.border, styles.savedFeed, isMobile && styles.savedFeedMobile]}
+      hoverStyle={pal.viewLight}
+      accessibilityLabel={displayName}
+      accessibilityHint=""
+      asAnchor
+      anchorNoUnderline>
+      <UserAvatar type="algo" size={28} avatar={avatar} />
+      <Text
+        type={isMobile ? 'lg' : 'lg-medium'}
+        style={[pal.text, s.flex1]}
+        numberOfLines={1}>
+        {displayName}
+      </Text>
+      {isMobile && (
+        <FontAwesomeIcon
+          icon="chevron-right"
+          size={14}
+          style={pal.textLight as FontAwesomeIconStyle}
+        />
+      )}
+    </Link>
+  )
+}
+
 const styles = StyleSheet.create({
   container: {
     flex: 1,
   },
+  list: {
+    height: '100%',
+  },
+  contentContainer: {
+    paddingBottom: 100,
+  },
+
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    gap: 16,
+    paddingHorizontal: 16,
+    paddingVertical: 12,
+  },
+
+  savedFeed: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingHorizontal: 16,
+    paddingVertical: 14,
+    gap: 12,
+    borderBottomWidth: 1,
+  },
+  savedFeedMobile: {
+    paddingVertical: 10,
+  },
 })