about summary refs log tree commit diff
path: root/src/view/com/posts/MultiFeed.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/posts/MultiFeed.tsx')
-rw-r--r--src/view/com/posts/MultiFeed.tsx230
1 files changed, 230 insertions, 0 deletions
diff --git a/src/view/com/posts/MultiFeed.tsx b/src/view/com/posts/MultiFeed.tsx
new file mode 100644
index 000000000..4911c9e2c
--- /dev/null
+++ b/src/view/com/posts/MultiFeed.tsx
@@ -0,0 +1,230 @@
+import React, {MutableRefObject} from 'react'
+import {observer} from 'mobx-react-lite'
+import {
+  ActivityIndicator,
+  RefreshControl,
+  StyleProp,
+  StyleSheet,
+  View,
+  ViewStyle,
+} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {FlatList} from '../util/Views'
+import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
+import {ErrorMessage} from '../util/error/ErrorMessage'
+import {PostsMultiFeedModel, MultiFeedItem} from 'state/models/feeds/multi-feed'
+import {FeedSlice} from './FeedSlice'
+import {Text} from '../util/text/Text'
+import {Link} from '../util/Link'
+import {UserAvatar} from '../util/UserAvatar'
+import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
+import {s} from 'lib/styles'
+import {useAnalytics} from 'lib/analytics'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useTheme} from 'lib/ThemeContext'
+
+export const MultiFeed = observer(function Feed({
+  multifeed,
+  style,
+  showPostFollowBtn,
+  scrollElRef,
+  onScroll,
+  scrollEventThrottle,
+  testID,
+  headerOffset = 0,
+  extraData,
+}: {
+  multifeed: PostsMultiFeedModel
+  style?: StyleProp<ViewStyle>
+  showPostFollowBtn?: boolean
+  scrollElRef?: MutableRefObject<FlatList<any> | null>
+  onPressTryAgain?: () => void
+  onScroll?: OnScrollCb
+  scrollEventThrottle?: number
+  renderEmptyState?: () => JSX.Element
+  testID?: string
+  headerOffset?: number
+  extraData?: any
+}) {
+  const pal = usePalette('default')
+  const palInverted = usePalette('inverted')
+  const theme = useTheme()
+  const {track} = useAnalytics()
+  const [isRefreshing, setIsRefreshing] = React.useState(false)
+
+  // events
+  // =
+
+  const onRefresh = React.useCallback(async () => {
+    track('MultiFeed:onRefresh')
+    setIsRefreshing(true)
+    try {
+      await multifeed.refresh()
+    } catch (err) {
+      multifeed.rootStore.log.error('Failed to refresh posts feed', err)
+    }
+    setIsRefreshing(false)
+  }, [multifeed, track, setIsRefreshing])
+
+  const onEndReached = React.useCallback(async () => {
+    track('MultiFeed:onEndReached')
+    try {
+      await multifeed.loadMore()
+    } catch (err) {
+      multifeed.rootStore.log.error('Failed to load more posts', err)
+    }
+  }, [multifeed, track])
+
+  // rendering
+  // =
+
+  const renderItem = React.useCallback(
+    ({item}: {item: MultiFeedItem}) => {
+      if (item.type === 'header') {
+        return <View style={[styles.header, pal.border]} />
+      } else if (item.type === 'feed-header') {
+        return (
+          <View style={styles.feedHeader}>
+            <UserAvatar type="algo" avatar={item.avatar} size={28} />
+            <Text type="title-lg" style={[pal.text, styles.feedHeaderTitle]}>
+              {item.title}
+            </Text>
+          </View>
+        )
+      } else if (item.type === 'feed-slice') {
+        return (
+          <FeedSlice slice={item.slice} showFollowBtn={showPostFollowBtn} />
+        )
+      } else if (item.type === 'feed-loading') {
+        return <PostFeedLoadingPlaceholder />
+      } else if (item.type === 'feed-error') {
+        return <ErrorMessage message={item.error} />
+      } else if (item.type === 'feed-footer') {
+        return (
+          <Link
+            href={item.uri}
+            style={[styles.feedFooter, pal.border, pal.view]}>
+            <Text type="lg" style={pal.link}>
+              See more from {item.title}
+            </Text>
+            <FontAwesomeIcon
+              icon="angle-right"
+              size={18}
+              color={pal.colors.link}
+            />
+          </Link>
+        )
+      } else if (item.type === 'footer') {
+        return (
+          <Link
+            style={[styles.footerLink, palInverted.view]}
+            href="/search/feeds">
+            <FontAwesomeIcon
+              icon="search"
+              size={18}
+              color={palInverted.colors.text}
+            />
+            <Text type="lg-medium" style={palInverted.text}>
+              Discover new feeds
+            </Text>
+          </Link>
+        )
+      }
+      return null
+    },
+    [showPostFollowBtn, pal, palInverted],
+  )
+
+  const FeedFooter = React.useCallback(
+    () =>
+      multifeed.isLoading && !isRefreshing ? (
+        <View style={styles.loadMore}>
+          <ActivityIndicator color={pal.colors.text} />
+        </View>
+      ) : (
+        <View />
+      ),
+    [multifeed.isLoading, isRefreshing, pal],
+  )
+
+  return (
+    <View testID={testID} style={style}>
+      {multifeed.items.length > 0 && (
+        <FlatList
+          testID={testID ? `${testID}-flatlist` : undefined}
+          ref={scrollElRef}
+          data={multifeed.items}
+          keyExtractor={item => item._reactKey}
+          renderItem={renderItem}
+          ListFooterComponent={FeedFooter}
+          refreshControl={
+            <RefreshControl
+              refreshing={isRefreshing}
+              onRefresh={onRefresh}
+              tintColor={pal.colors.text}
+              titleColor={pal.colors.text}
+              progressViewOffset={headerOffset}
+            />
+          }
+          contentContainerStyle={s.contentContainer}
+          style={[{paddingTop: headerOffset}, pal.viewLight, styles.container]}
+          onScroll={onScroll}
+          scrollEventThrottle={scrollEventThrottle}
+          indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
+          onEndReached={onEndReached}
+          onEndReachedThreshold={0.6}
+          removeClippedSubviews={true}
+          contentOffset={{x: 0, y: headerOffset * -1}}
+          extraData={extraData}
+          // @ts-ignore our .web version only -prf
+          desktopFixedHeight
+        />
+      )}
+    </View>
+  )
+})
+
+const styles = StyleSheet.create({
+  container: {
+    height: '100%',
+  },
+  header: {
+    borderTopWidth: 1,
+    marginBottom: 4,
+  },
+  feedHeader: {
+    flexDirection: 'row',
+    gap: 8,
+    alignItems: 'center',
+    paddingHorizontal: 16,
+    paddingBottom: 8,
+    marginTop: 12,
+  },
+  feedHeaderTitle: {
+    fontWeight: 'bold',
+  },
+  feedFooter: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    paddingHorizontal: 16,
+    paddingVertical: 16,
+    marginBottom: 12,
+    borderTopWidth: 1,
+    borderBottomWidth: 1,
+  },
+  footerLink: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    borderRadius: 8,
+    paddingHorizontal: 14,
+    paddingVertical: 12,
+    marginHorizontal: 8,
+    marginBottom: 8,
+    gap: 8,
+  },
+  loadMore: {
+    paddingTop: 10,
+  },
+})