about summary refs log tree commit diff
path: root/src/view/com
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com')
-rw-r--r--src/view/com/discover/SuggestedFollows.tsx68
-rw-r--r--src/view/com/search/Suggestions.tsx290
2 files changed, 240 insertions, 118 deletions
diff --git a/src/view/com/discover/SuggestedFollows.tsx b/src/view/com/discover/SuggestedFollows.tsx
deleted file mode 100644
index ae5605c5c..000000000
--- a/src/view/com/discover/SuggestedFollows.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import React from 'react'
-import {StyleSheet, View} from 'react-native'
-import {AppBskyActorDefs} from '@atproto/api'
-import {RefWithInfoAndFollowers} from 'state/models/discovery/foafs'
-import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
-import {Text} from '../util/text/Text'
-import {usePalette} from 'lib/hooks/usePalette'
-
-export const SuggestedFollows = ({
-  title,
-  suggestions,
-}: {
-  title: string
-  suggestions: (
-    | AppBskyActorDefs.ProfileViewBasic
-    | AppBskyActorDefs.ProfileView
-    | RefWithInfoAndFollowers
-  )[]
-}) => {
-  const pal = usePalette('default')
-  return (
-    <View style={[styles.container, pal.view, pal.border]}>
-      <Text type="title" style={[styles.heading, pal.text]}>
-        {title}
-      </Text>
-      {suggestions.map(item => (
-        <View key={item.did} style={[styles.card, pal.view, pal.border]}>
-          <ProfileCardWithFollowBtn
-            key={item.did}
-            did={item.did}
-            handle={item.handle}
-            displayName={item.displayName}
-            avatar={item.avatar}
-            labels={item.labels}
-            noBg
-            noBorder
-            description={
-              item.description
-                ? (item as AppBskyActorDefs.ProfileView).description
-                : ''
-            }
-            followers={
-              item.followers
-                ? (item.followers as AppBskyActorDefs.ProfileView[])
-                : undefined
-            }
-          />
-        </View>
-      ))}
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  container: {
-    borderBottomWidth: 1,
-  },
-
-  heading: {
-    fontWeight: 'bold',
-    paddingHorizontal: 12,
-    paddingBottom: 8,
-  },
-
-  card: {
-    borderTopWidth: 1,
-  },
-})
diff --git a/src/view/com/search/Suggestions.tsx b/src/view/com/search/Suggestions.tsx
index e9999e1d2..c1355bfb5 100644
--- a/src/view/com/search/Suggestions.tsx
+++ b/src/view/com/search/Suggestions.tsx
@@ -1,65 +1,255 @@
-import React from 'react'
-import {StyleSheet, View} from 'react-native'
+import React, {forwardRef, ForwardedRef} from 'react'
+import {RefreshControl, StyleSheet, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
+import {AppBskyActorDefs} from '@atproto/api'
+import {CenteredView, FlatList} from '../util/Views'
 import {FoafsModel} from 'state/models/discovery/foafs'
-import {SuggestedActorsModel} from 'state/models/discovery/suggested-actors'
-import {SuggestedFollows} from 'view/com/discover/SuggestedFollows'
+import {
+  SuggestedActorsModel,
+  SuggestedActor,
+} from 'state/models/discovery/suggested-actors'
+import {Text} from '../util/text/Text'
+import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
 import {ProfileCardFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {RefWithInfoAndFollowers} from 'state/models/discovery/foafs'
+import {usePalette} from 'lib/hooks/usePalette'
+
+interface Heading {
+  _reactKey: string
+  type: 'heading'
+  title: string
+}
+interface RefWrapper {
+  _reactKey: string
+  type: 'ref'
+  ref: RefWithInfoAndFollowers
+}
+interface SuggestWrapper {
+  _reactKey: string
+  type: 'suggested'
+  suggested: SuggestedActor
+}
+interface ProfileView {
+  _reactKey: string
+  type: 'profile-view'
+  view: AppBskyActorDefs.ProfileViewBasic
+}
+type Item = Heading | RefWrapper | SuggestWrapper | ProfileView
 
 export const Suggestions = observer(
-  ({
-    foafs,
-    suggestedActors,
-  }: {
-    foafs: FoafsModel
-    suggestedActors: SuggestedActorsModel
-  }) => {
-    if (foafs.isLoading || suggestedActors.isLoading) {
-      return <ProfileCardFeedLoadingPlaceholder />
-    }
-    return (
-      <>
-        {foafs.popular.length > 0 && (
-          <View style={styles.suggestions}>
-            <SuggestedFollows
-              title="In your network"
-              suggestions={foafs.popular}
-            />
-          </View>
-        )}
-        {suggestedActors.hasContent && (
-          <View style={styles.suggestions}>
-            <SuggestedFollows
-              title="Suggested follows"
-              suggestions={suggestedActors.suggestions}
-            />
-          </View>
-        )}
-        {foafs.sources.map((source, i) => {
+  forwardRef(
+    (
+      {
+        foafs,
+        suggestedActors,
+      }: {
+        foafs: FoafsModel
+        suggestedActors: SuggestedActorsModel
+      },
+      flatListRef: ForwardedRef<FlatList>,
+    ) => {
+      const pal = usePalette('default')
+      const [refreshing, setRefreshing] = React.useState(false)
+      const data = React.useMemo(() => {
+        let items: Item[] = []
+
+        if (foafs.popular.length > 0) {
+          items = items
+            .concat([
+              {
+                _reactKey: '__popular_heading__',
+                type: 'heading',
+                title: 'In your network',
+              },
+            ])
+            .concat(
+              foafs.popular.map(ref => ({
+                _reactKey: `popular-${ref.did}`,
+                type: 'ref',
+                ref,
+              })),
+            )
+        }
+        if (suggestedActors.hasContent) {
+          items = items
+            .concat([
+              {
+                _reactKey: '__suggested_heading__',
+                type: 'heading',
+                title: 'Suggested follows',
+              },
+            ])
+            .concat(
+              suggestedActors.suggestions.map(suggested => ({
+                _reactKey: `suggested-${suggested.did}`,
+                type: 'suggested',
+                suggested,
+              })),
+            )
+        }
+        for (const source of foafs.sources) {
           const item = foafs.foafs.get(source)
           if (!item || item.follows.length === 0) {
-            return <View key={`sf-${item?.did || i}`} />
+            return
           }
-          return (
-            <View key={`sf-${item.did}`} style={styles.suggestions}>
-              <SuggestedFollows
-                title={`Followed by ${sanitizeDisplayName(
+          items = items
+            .concat([
+              {
+                _reactKey: `__${item.did}_heading__`,
+                type: 'heading',
+                title: `Followed by ${sanitizeDisplayName(
                   item.displayName || item.handle,
-                )}`}
-                suggestions={item.follows.slice(0, 10)}
-              />
-            </View>
-          )
-        })}
-      </>
-    )
-  },
+                )}`,
+              },
+            ])
+            .concat(
+              item.follows.slice(0, 10).map(view => ({
+                _reactKey: `${item.did}-${view.did}`,
+                type: 'profile-view',
+                view,
+              })),
+            )
+        }
+
+        return items
+      }, [
+        foafs.popular,
+        suggestedActors.hasContent,
+        suggestedActors.suggestions,
+        foafs.sources,
+        foafs.foafs,
+      ])
+
+      const onRefresh = React.useCallback(async () => {
+        setRefreshing(true)
+        try {
+          await foafs.fetch()
+        } finally {
+          setRefreshing(false)
+        }
+      }, [foafs, setRefreshing])
+
+      const renderItem = React.useCallback(
+        ({item}: {item: Item}) => {
+          if (item.type === 'heading') {
+            return (
+              <Text type="title" style={[styles.heading, pal.text]}>
+                {item.title}
+              </Text>
+            )
+          }
+          if (item.type === 'ref') {
+            return (
+              <View style={[styles.card, pal.view, pal.border]}>
+                <ProfileCardWithFollowBtn
+                  key={item.ref.did}
+                  did={item.ref.did}
+                  handle={item.ref.handle}
+                  displayName={item.ref.displayName}
+                  avatar={item.ref.avatar}
+                  labels={item.ref.labels}
+                  noBg
+                  noBorder
+                  description={
+                    item.ref.description
+                      ? (item.ref as AppBskyActorDefs.ProfileView).description
+                      : ''
+                  }
+                  followers={
+                    item.ref.followers
+                      ? (item.ref.followers as AppBskyActorDefs.ProfileView[])
+                      : undefined
+                  }
+                />
+              </View>
+            )
+          }
+          if (item.type === 'profile-view') {
+            return (
+              <View style={[styles.card, pal.view, pal.border]}>
+                <ProfileCardWithFollowBtn
+                  key={item.view.did}
+                  did={item.view.did}
+                  handle={item.view.handle}
+                  displayName={item.view.displayName}
+                  avatar={item.view.avatar}
+                  labels={item.view.labels}
+                  noBg
+                  noBorder
+                  description={
+                    item.view.description
+                      ? (item.view as AppBskyActorDefs.ProfileView).description
+                      : ''
+                  }
+                />
+              </View>
+            )
+          }
+          if (item.type === 'suggested') {
+            return (
+              <View style={[styles.card, pal.view, pal.border]}>
+                <ProfileCardWithFollowBtn
+                  key={item.suggested.did}
+                  did={item.suggested.did}
+                  handle={item.suggested.handle}
+                  displayName={item.suggested.displayName}
+                  avatar={item.suggested.avatar}
+                  labels={item.suggested.labels}
+                  noBg
+                  noBorder
+                  description={
+                    item.suggested.description
+                      ? (item.suggested as AppBskyActorDefs.ProfileView)
+                          .description
+                      : ''
+                  }
+                />
+              </View>
+            )
+          }
+          return null
+        },
+        [pal],
+      )
+
+      if (foafs.isLoading || suggestedActors.isLoading) {
+        return (
+          <CenteredView>
+            <ProfileCardFeedLoadingPlaceholder />
+          </CenteredView>
+        )
+      }
+      return (
+        <FlatList
+          ref={flatListRef}
+          data={data}
+          keyExtractor={item => item._reactKey}
+          refreshControl={
+            <RefreshControl
+              refreshing={refreshing}
+              onRefresh={onRefresh}
+              tintColor={pal.colors.text}
+              titleColor={pal.colors.text}
+            />
+          }
+          renderItem={renderItem}
+          initialNumToRender={15}
+        />
+      )
+    },
+  ),
 )
 
 const styles = StyleSheet.create({
-  suggestions: {
-    marginTop: 10,
-    marginBottom: 20,
+  heading: {
+    fontWeight: 'bold',
+    paddingHorizontal: 12,
+    paddingBottom: 8,
+    paddingTop: 16,
+  },
+
+  card: {
+    borderTopWidth: 1,
   },
 })