about summary refs log tree commit diff
diff options
context:
space:
mode:
authordan <dan.abramov@gmail.com>2024-11-19 15:24:08 +0000
committerGitHub <noreply@github.com>2024-11-19 15:24:08 +0000
commitcfc653a5f4d5e49917cf816a86fe3f027a625a07 (patch)
treed5823de336646cfff32c963fb03b652f04144155
parent8d5e61e1e5a542dd99f32d471226bd47a523e22d (diff)
downloadvoidsky-cfc653a5f4d5e49917cf816a86fe3f027a625a07.tar.zst
Split FeedSlice into FlatList rows (#6507)
-rw-r--r--src/view/com/posts/Feed.tsx143
-rw-r--r--src/view/com/posts/FeedSlice.tsx184
-rw-r--r--src/view/com/posts/ViewFullThread.tsx72
3 files changed, 188 insertions, 211 deletions
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx
index d6cf6dac5..07c1fd6b7 100644
--- a/src/view/com/posts/Feed.tsx
+++ b/src/view/com/posts/Feed.tsx
@@ -42,10 +42,11 @@ import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
 import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
 import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
 import {FeedErrorMessage} from './FeedErrorMessage'
+import {FeedItem} from './FeedItem'
 import {FeedShutdownMsg} from './FeedShutdownMsg'
-import {FeedSlice} from './FeedSlice'
+import {ViewFullThread} from './ViewFullThread'
 
-type FeedItem =
+type FeedRow =
   | {
       type: 'loading'
       key: string
@@ -72,6 +73,18 @@ type FeedItem =
       slice: FeedPostSlice
     }
   | {
+      type: 'sliceItem'
+      key: string
+      slice: FeedPostSlice
+      indexInSlice: number
+      showReplyTo: boolean
+    }
+  | {
+      type: 'sliceViewFullThread'
+      key: string
+      uri: string
+    }
+  | {
       type: 'interstitialFeeds'
       key: string
       params: {
@@ -101,7 +114,7 @@ const followInterstitialType = 'interstitialFollows'
 const progressGuideInterstitialType = 'interstitialProgressGuide'
 const interstials: Record<
   'following' | 'discover' | 'profile',
-  (FeedItem & {
+  (FeedRow & {
     type:
       | 'interstitialFeeds'
       | 'interstitialFollows'
@@ -139,9 +152,9 @@ const interstials: Record<
   ],
 }
 
-export function getFeedPostSlice(feedItem: FeedItem): FeedPostSlice | null {
-  if (feedItem.type === 'slice') {
-    return feedItem.slice
+export function getFeedPostSlice(feedRow: FeedRow): FeedPostSlice | null {
+  if (feedRow.type === 'sliceItem') {
+    return feedRow.slice
   } else {
     return null
   }
@@ -303,8 +316,8 @@ let Feed = ({
     }
   }, [pollInterval])
 
-  const feedItems: FeedItem[] = React.useMemo(() => {
-    let arr: FeedItem[] = []
+  const feedItems: FeedRow[] = React.useMemo(() => {
+    let arr: FeedRow[] = []
     if (KNOWN_SHUTDOWN_FEEDS.includes(feedUri)) {
       arr.push({
         type: 'feedShutdownMsg',
@@ -324,13 +337,50 @@ let Feed = ({
         })
       } else if (data) {
         for (const page of data?.pages) {
-          arr = arr.concat(
-            page.slices.map(s => ({
-              type: 'slice',
-              slice: s,
-              key: s._reactKey,
-            })),
-          )
+          for (const slice of page.slices) {
+            if (slice.isIncompleteThread && slice.items.length >= 3) {
+              const beforeLast = slice.items.length - 2
+              const last = slice.items.length - 1
+              arr.push({
+                type: 'sliceItem',
+                key: slice.items[0]._reactKey,
+                slice: slice,
+                indexInSlice: 0,
+                showReplyTo: false,
+              })
+              arr.push({
+                type: 'sliceViewFullThread',
+                key: slice._reactKey + '-viewFullThread',
+                uri: slice.items[0].uri,
+              })
+              arr.push({
+                type: 'sliceItem',
+                key: slice.items[beforeLast]._reactKey,
+                slice: slice,
+                indexInSlice: beforeLast,
+                showReplyTo:
+                  slice.items[beforeLast].parentAuthor?.did !==
+                  slice.items[beforeLast].post.author.did,
+              })
+              arr.push({
+                type: 'sliceItem',
+                key: slice.items[last]._reactKey,
+                slice: slice,
+                indexInSlice: last,
+                showReplyTo: false,
+              })
+            } else {
+              for (let i = 0; i < slice.items.length; i++) {
+                arr.push({
+                  type: 'sliceItem',
+                  key: slice.items[i]._reactKey,
+                  slice: slice,
+                  indexInSlice: i,
+                  showReplyTo: i === 0,
+                })
+              }
+            }
+          }
         }
       }
       if (isError && !isEmpty) {
@@ -454,10 +504,10 @@ let Feed = ({
   // =
 
   const renderItem = React.useCallback(
-    ({item, index}: ListRenderItemInfo<FeedItem>) => {
-      if (item.type === 'empty') {
+    ({item: row, index: rowIndex}: ListRenderItemInfo<FeedRow>) => {
+      if (row.type === 'empty') {
         return renderEmptyState()
-      } else if (item.type === 'error') {
+      } else if (row.type === 'error') {
         return (
           <FeedErrorMessage
             feedDesc={feed}
@@ -466,7 +516,7 @@ let Feed = ({
             savedFeedConfig={savedFeedConfig}
           />
         )
-      } else if (item.type === 'loadMoreError') {
+      } else if (row.type === 'loadMoreError') {
         return (
           <LoadMoreRetryBtn
             label={_(
@@ -475,25 +525,50 @@ let Feed = ({
             onPress={onPressRetryLoadMore}
           />
         )
-      } else if (item.type === 'loading') {
+      } else if (row.type === 'loading') {
         return <PostFeedLoadingPlaceholder />
-      } else if (item.type === 'feedShutdownMsg') {
+      } else if (row.type === 'feedShutdownMsg') {
         return <FeedShutdownMsg feedUri={feedUri} />
-      } else if (item.type === feedInterstitialType) {
+      } else if (row.type === feedInterstitialType) {
         return <SuggestedFeeds />
-      } else if (item.type === followInterstitialType) {
+      } else if (row.type === followInterstitialType) {
         return <SuggestedFollows feed={feed} />
-      } else if (item.type === progressGuideInterstitialType) {
+      } else if (row.type === progressGuideInterstitialType) {
         return <ProgressGuide />
-      } else if (item.type === 'slice') {
-        if (item.slice.isFallbackMarker) {
+      } else if (row.type === 'sliceItem') {
+        const slice = row.slice
+        if (slice.isFallbackMarker) {
           // HACK
           // tell the user we fell back to discover
           // see home.ts (feed api) for more info
           // -prf
           return <DiscoverFallbackHeader />
         }
-        return <FeedSlice slice={item.slice} hideTopBorder={index === 0} />
+        const indexInSlice = row.indexInSlice
+        const item = slice.items[indexInSlice]
+        return (
+          <FeedItem
+            post={item.post}
+            record={item.record}
+            reason={indexInSlice === 0 ? slice.reason : undefined}
+            feedContext={slice.feedContext}
+            moderation={item.moderation}
+            parentAuthor={item.parentAuthor}
+            showReplyTo={row.showReplyTo}
+            isThreadParent={isThreadParentAt(slice.items, indexInSlice)}
+            isThreadChild={isThreadChildAt(slice.items, indexInSlice)}
+            isThreadLastChild={
+              isThreadChildAt(slice.items, indexInSlice) &&
+              slice.items.length === indexInSlice + 1
+            }
+            isParentBlocked={item.isParentBlocked}
+            isParentNotFound={item.isParentNotFound}
+            hideTopBorder={rowIndex === 0 && indexInSlice === 0}
+            rootPost={slice.items[0].post}
+          />
+        )
+      } else if (row.type === 'sliceViewFullThread') {
+        return <ViewFullThread uri={row.uri} />
       } else {
         return null
       }
@@ -574,3 +649,17 @@ export {Feed}
 const styles = StyleSheet.create({
   feedFooter: {paddingTop: 20},
 })
+
+function isThreadParentAt<T>(arr: Array<T>, i: number) {
+  if (arr.length === 1) {
+    return false
+  }
+  return i < arr.length - 1
+}
+
+function isThreadChildAt<T>(arr: Array<T>, i: number) {
+  if (arr.length === 1) {
+    return false
+  }
+  return i > 0
+}
diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx
deleted file mode 100644
index 09335fa0e..000000000
--- a/src/view/com/posts/FeedSlice.tsx
+++ /dev/null
@@ -1,184 +0,0 @@
-import React, {memo} from 'react'
-import {StyleSheet, View} from 'react-native'
-import Svg, {Circle, Line} from 'react-native-svg'
-import {AtUri} from '@atproto/api'
-import {Trans} from '@lingui/macro'
-
-import {usePalette} from '#/lib/hooks/usePalette'
-import {makeProfileLink} from '#/lib/routes/links'
-import {FeedPostSlice} from '#/state/queries/post-feed'
-import {useInteractionState} from '#/components/hooks/useInteractionState'
-import {SubtleWebHover} from '#/components/SubtleWebHover'
-import {Link} from '../util/Link'
-import {Text} from '../util/text/Text'
-import {FeedItem} from './FeedItem'
-
-let FeedSlice = ({
-  slice,
-  hideTopBorder,
-}: {
-  slice: FeedPostSlice
-  hideTopBorder?: boolean
-}): React.ReactNode => {
-  if (slice.isIncompleteThread && slice.items.length >= 3) {
-    const beforeLast = slice.items.length - 2
-    const last = slice.items.length - 1
-    return (
-      <>
-        <FeedItem
-          key={slice.items[0]._reactKey}
-          post={slice.items[0].post}
-          record={slice.items[0].record}
-          reason={slice.reason}
-          feedContext={slice.feedContext}
-          parentAuthor={slice.items[0].parentAuthor}
-          showReplyTo={false}
-          moderation={slice.items[0].moderation}
-          isThreadParent={isThreadParentAt(slice.items, 0)}
-          isThreadChild={isThreadChildAt(slice.items, 0)}
-          hideTopBorder={hideTopBorder}
-          isParentBlocked={slice.items[0].isParentBlocked}
-          isParentNotFound={slice.items[0].isParentNotFound}
-          rootPost={slice.items[0].post}
-        />
-        <ViewFullThread uri={slice.items[0].uri} />
-        <FeedItem
-          key={slice.items[beforeLast]._reactKey}
-          post={slice.items[beforeLast].post}
-          record={slice.items[beforeLast].record}
-          reason={undefined}
-          feedContext={slice.feedContext}
-          parentAuthor={slice.items[beforeLast].parentAuthor}
-          showReplyTo={
-            slice.items[beforeLast].parentAuthor?.did !==
-            slice.items[beforeLast].post.author.did
-          }
-          moderation={slice.items[beforeLast].moderation}
-          isThreadParent={isThreadParentAt(slice.items, beforeLast)}
-          isThreadChild={isThreadChildAt(slice.items, beforeLast)}
-          isParentBlocked={slice.items[beforeLast].isParentBlocked}
-          isParentNotFound={slice.items[beforeLast].isParentNotFound}
-          rootPost={slice.items[0].post}
-        />
-        <FeedItem
-          key={slice.items[last]._reactKey}
-          post={slice.items[last].post}
-          record={slice.items[last].record}
-          reason={undefined}
-          feedContext={slice.feedContext}
-          parentAuthor={slice.items[last].parentAuthor}
-          showReplyTo={false}
-          moderation={slice.items[last].moderation}
-          isThreadParent={isThreadParentAt(slice.items, last)}
-          isThreadChild={isThreadChildAt(slice.items, last)}
-          isParentBlocked={slice.items[last].isParentBlocked}
-          isParentNotFound={slice.items[last].isParentNotFound}
-          isThreadLastChild
-          rootPost={slice.items[0].post}
-        />
-      </>
-    )
-  }
-
-  return (
-    <>
-      {slice.items.map((item, i) => (
-        <FeedItem
-          key={item._reactKey}
-          post={slice.items[i].post}
-          record={slice.items[i].record}
-          reason={i === 0 ? slice.reason : undefined}
-          feedContext={slice.feedContext}
-          moderation={slice.items[i].moderation}
-          parentAuthor={slice.items[i].parentAuthor}
-          showReplyTo={i === 0}
-          isThreadParent={isThreadParentAt(slice.items, i)}
-          isThreadChild={isThreadChildAt(slice.items, i)}
-          isThreadLastChild={
-            isThreadChildAt(slice.items, i) && slice.items.length === i + 1
-          }
-          isParentBlocked={slice.items[i].isParentBlocked}
-          isParentNotFound={slice.items[i].isParentNotFound}
-          hideTopBorder={hideTopBorder && i === 0}
-          rootPost={slice.items[0].post}
-        />
-      ))}
-    </>
-  )
-}
-FeedSlice = memo(FeedSlice)
-export {FeedSlice}
-
-function ViewFullThread({uri}: {uri: string}) {
-  const {
-    state: hover,
-    onIn: onHoverIn,
-    onOut: onHoverOut,
-  } = useInteractionState()
-  const pal = usePalette('default')
-  const itemHref = React.useMemo(() => {
-    const urip = new AtUri(uri)
-    return makeProfileLink({did: urip.hostname, handle: ''}, 'post', urip.rkey)
-  }, [uri])
-
-  return (
-    <Link
-      style={[styles.viewFullThread]}
-      href={itemHref}
-      asAnchor
-      noFeedback
-      onPointerEnter={onHoverIn}
-      onPointerLeave={onHoverOut}>
-      <SubtleWebHover
-        hover={hover}
-        // adjust position for visual alignment - the actual box has lots of top padding and not much bottom padding -sfn
-        style={{top: 8, bottom: -5}}
-      />
-      <View style={styles.viewFullThreadDots}>
-        <Svg width="4" height="40">
-          <Line
-            x1="2"
-            y1="0"
-            x2="2"
-            y2="15"
-            stroke={pal.colors.replyLine}
-            strokeWidth="2"
-          />
-          <Circle cx="2" cy="22" r="1.5" fill={pal.colors.replyLineDot} />
-          <Circle cx="2" cy="28" r="1.5" fill={pal.colors.replyLineDot} />
-          <Circle cx="2" cy="34" r="1.5" fill={pal.colors.replyLineDot} />
-        </Svg>
-      </View>
-
-      <Text type="md" style={[pal.link, {paddingTop: 18, paddingBottom: 4}]}>
-        <Trans>View full thread</Trans>
-      </Text>
-    </Link>
-  )
-}
-
-const styles = StyleSheet.create({
-  viewFullThread: {
-    flexDirection: 'row',
-    gap: 10,
-    paddingLeft: 18,
-  },
-  viewFullThreadDots: {
-    width: 42,
-    alignItems: 'center',
-  },
-})
-
-function isThreadParentAt<T>(arr: Array<T>, i: number) {
-  if (arr.length === 1) {
-    return false
-  }
-  return i < arr.length - 1
-}
-
-function isThreadChildAt<T>(arr: Array<T>, i: number) {
-  if (arr.length === 1) {
-    return false
-  }
-  return i > 0
-}
diff --git a/src/view/com/posts/ViewFullThread.tsx b/src/view/com/posts/ViewFullThread.tsx
new file mode 100644
index 000000000..0b347f22c
--- /dev/null
+++ b/src/view/com/posts/ViewFullThread.tsx
@@ -0,0 +1,72 @@
+import React from 'react'
+import {StyleSheet, View} from 'react-native'
+import Svg, {Circle, Line} from 'react-native-svg'
+import {AtUri} from '@atproto/api'
+import {Trans} from '@lingui/macro'
+
+import {usePalette} from '#/lib/hooks/usePalette'
+import {makeProfileLink} from '#/lib/routes/links'
+import {useInteractionState} from '#/components/hooks/useInteractionState'
+import {SubtleWebHover} from '#/components/SubtleWebHover'
+import {Link} from '../util/Link'
+import {Text} from '../util/text/Text'
+
+export function ViewFullThread({uri}: {uri: string}) {
+  const {
+    state: hover,
+    onIn: onHoverIn,
+    onOut: onHoverOut,
+  } = useInteractionState()
+  const pal = usePalette('default')
+  const itemHref = React.useMemo(() => {
+    const urip = new AtUri(uri)
+    return makeProfileLink({did: urip.hostname, handle: ''}, 'post', urip.rkey)
+  }, [uri])
+
+  return (
+    <Link
+      style={[styles.viewFullThread]}
+      href={itemHref}
+      asAnchor
+      noFeedback
+      onPointerEnter={onHoverIn}
+      onPointerLeave={onHoverOut}>
+      <SubtleWebHover
+        hover={hover}
+        // adjust position for visual alignment - the actual box has lots of top padding and not much bottom padding -sfn
+        style={{top: 8, bottom: -5}}
+      />
+      <View style={styles.viewFullThreadDots}>
+        <Svg width="4" height="40">
+          <Line
+            x1="2"
+            y1="0"
+            x2="2"
+            y2="15"
+            stroke={pal.colors.replyLine}
+            strokeWidth="2"
+          />
+          <Circle cx="2" cy="22" r="1.5" fill={pal.colors.replyLineDot} />
+          <Circle cx="2" cy="28" r="1.5" fill={pal.colors.replyLineDot} />
+          <Circle cx="2" cy="34" r="1.5" fill={pal.colors.replyLineDot} />
+        </Svg>
+      </View>
+
+      <Text type="md" style={[pal.link, {paddingTop: 18, paddingBottom: 4}]}>
+        <Trans>View full thread</Trans>
+      </Text>
+    </Link>
+  )
+}
+
+const styles = StyleSheet.create({
+  viewFullThread: {
+    flexDirection: 'row',
+    gap: 10,
+    paddingLeft: 18,
+  },
+  viewFullThreadDots: {
+    width: 42,
+    alignItems: 'center',
+  },
+})