From 9edb4879494b348616caca6999ee89658f439c49 Mon Sep 17 00:00:00 2001
From: Hailey <me@haileyok.com>
Date: Wed, 29 May 2024 20:28:32 -0700
Subject: Always show the header on post threads on native (#4254)

* always show header on native

* ALF ALF ALF

* rm offset for top border

* wrap in a `CenteredView`

* use `CenteredView`'s side borders

* account for loading state on web

* move `isTabletOrMobile`

* hide top border on first post in list

* show border if parents are loading

* don't show top border for deleted or blocked posts

* hide top border for hidden replies

* Rm root post top border

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
---
 src/view/com/post-thread/PostThread.tsx | 337 +++++++++++++++-----------------
 1 file changed, 155 insertions(+), 182 deletions(-)

(limited to 'src/view/com/post-thread/PostThread.tsx')

diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index 4f7d0d3c6..1212f992d 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -1,5 +1,5 @@
 import React, {useEffect, useRef} from 'react'
-import {StyleSheet, useWindowDimensions, View} from 'react-native'
+import {useWindowDimensions, View} from 'react-native'
 import {runOnJS} from 'react-native-reanimated'
 import {AppBskyFeedDefs} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
@@ -22,15 +22,16 @@ import {
 import {usePreferencesQuery} from '#/state/queries/preferences'
 import {useSession} from '#/state/session'
 import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
-import {usePalette} from 'lib/hooks/usePalette'
 import {useSetTitle} from 'lib/hooks/useSetTitle'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {cleanError} from 'lib/strings/errors'
+import {CenteredView} from 'view/com/util/Views'
+import {atoms as a, useTheme} from '#/alf'
 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
+import {Text} from '#/components/Typography'
 import {ComposePrompt} from '../composer/Prompt'
 import {List, ListMethods} from '../util/List'
-import {Text} from '../util/text/Text'
 import {ViewHeader} from '../util/ViewHeader'
 import {PostThreadItem} from './PostThreadItem'
 import {PostThreadShowHiddenReplies} from './PostThreadShowHiddenReplies'
@@ -45,7 +46,6 @@ const MAINTAIN_VISIBLE_CONTENT_POSITION = {
   minIndexForVisible: 0,
 }
 
-const TOP_COMPONENT = {_reactKey: '__top_component__'}
 const REPLY_PROMPT = {_reactKey: '__reply__'}
 const LOAD_MORE = {_reactKey: '__load_more__'}
 const SHOW_HIDDEN_REPLIES = {_reactKey: '__show_hidden_replies__'}
@@ -66,7 +66,6 @@ type YieldedItem =
 type RowItem =
   | YieldedItem
   // TODO: TS doesn't actually enforce it's one of these, it only enforces matching shape.
-  | typeof TOP_COMPONENT
   | typeof REPLY_PROMPT
   | typeof LOAD_MORE
 
@@ -91,7 +90,7 @@ export function PostThread({
 }) {
   const {hasSession} = useSession()
   const {_} = useLingui()
-  const pal = usePalette('default')
+  const t = useTheme()
   const {isMobile, isTabletOrMobile} = useWebMediaQueries()
   const initialNumToRender = useInitialNumToRender()
   const {height: windowHeight} = useWindowDimensions()
@@ -224,32 +223,21 @@ export function PostThread({
     const {parents, highlightedPost, replies} = skeleton
     let arr: RowItem[] = []
     if (highlightedPost.type === 'post') {
-      const isRoot =
-        !highlightedPost.parent && !highlightedPost.ctx.isParentLoading
-      if (isRoot) {
-        // No parents to load.
-        arr.push(TOP_COMPONENT)
-      } else {
-        if (highlightedPost.ctx.isParentLoading || deferParents) {
-          // We're loading parents of the highlighted post.
-          // In this case, we don't render anything above the post.
-          // If you add something here, you'll need to update both
-          // maintainVisibleContentPosition and onContentSizeChange
-          // to "hold onto" the correct row instead of the first one.
-        } else {
-          // Everything is loaded
-          let startIndex = Math.max(0, parents.length - maxParents)
-          if (startIndex === 0) {
-            arr.push(TOP_COMPONENT)
-          } else {
-            // When progressively revealing parents, rendering a placeholder
-            // here will cause scrolling jumps. Don't add it unless you test it.
-            // QT'ing this thread is a great way to test all the scrolling hacks:
-            // https://bsky.app/profile/www.mozzius.dev/post/3kjqhblh6qk2o
-          }
-          for (let i = startIndex; i < parents.length; i++) {
-            arr.push(parents[i])
-          }
+      // We want to wait for parents to load before rendering.
+      // If you add something here, you'll need to update both
+      // maintainVisibleContentPosition and onContentSizeChange
+      // to "hold onto" the correct row instead of the first one.
+
+      if (!highlightedPost.ctx.isParentLoading && !deferParents) {
+        // When progressively revealing parents, rendering a placeholder
+        // here will cause scrolling jumps. Don't add it unless you test it.
+        // QT'ing this thread is a great way to test all the scrolling hacks:
+        // https://bsky.app/profile/www.mozzius.dev/post/3kjqhblh6qk2o
+
+        // Everything is loaded
+        let startIndex = Math.max(0, parents.length - maxParents)
+        for (let i = startIndex; i < parents.length; i++) {
+          arr.push(parents[i])
         }
       }
       arr.push(highlightedPost)
@@ -323,117 +311,100 @@ export function PostThread({
     setMaxReplies(prev => prev + 50)
   }, [isFetching, maxReplies, posts.length])
 
-  const renderItem = React.useCallback(
-    ({item, index}: {item: RowItem; index: number}) => {
-      if (item === TOP_COMPONENT) {
-        return isTabletOrMobile ? (
-          <ViewHeader
-            title={_(msg({message: `Post`, context: 'description'}))}
-          />
-        ) : null
-      } else if (item === REPLY_PROMPT && hasSession) {
-        return (
-          <View>
-            {!isMobile && <ComposePrompt onPressCompose={onPressReply} />}
-          </View>
-        )
-      } else if (item === SHOW_HIDDEN_REPLIES) {
-        return (
-          <PostThreadShowHiddenReplies
-            type="hidden"
-            onPress={() =>
-              setHiddenRepliesState(HiddenRepliesState.ShowAndOverridePostHider)
-            }
-          />
-        )
-      } else if (item === SHOW_MUTED_REPLIES) {
-        return (
-          <PostThreadShowHiddenReplies
-            type="muted"
-            onPress={() =>
-              setHiddenRepliesState(HiddenRepliesState.ShowAndOverridePostHider)
+  const hasParents =
+    skeleton?.highlightedPost?.type === 'post' &&
+    (skeleton.highlightedPost.ctx.isParentLoading ||
+      Boolean(skeleton?.parents && skeleton.parents.length > 0))
+  const showHeader =
+    isNative || (isTabletOrMobile && (!hasParents || !isFetching))
+
+  const renderItem = ({item, index}: {item: RowItem; index: number}) => {
+    if (item === REPLY_PROMPT && hasSession) {
+      return (
+        <View>
+          {!isMobile && <ComposePrompt onPressCompose={onPressReply} />}
+        </View>
+      )
+    } else if (item === SHOW_HIDDEN_REPLIES || item === SHOW_MUTED_REPLIES) {
+      return (
+        <PostThreadShowHiddenReplies
+          type={item === SHOW_HIDDEN_REPLIES ? 'hidden' : 'muted'}
+          onPress={() =>
+            setHiddenRepliesState(HiddenRepliesState.ShowAndOverridePostHider)
+          }
+          hideTopBorder={index === 0}
+        />
+      )
+    } else if (isThreadNotFound(item)) {
+      return (
+        <View
+          style={[
+            a.p_lg,
+            index !== 0 && a.border_t,
+            t.atoms.border_contrast_low,
+            t.atoms.bg_contrast_25,
+          ]}>
+          <Text style={[a.font_bold, a.text_md, t.atoms.text_contrast_medium]}>
+            <Trans>Deleted post.</Trans>
+          </Text>
+        </View>
+      )
+    } else if (isThreadBlocked(item)) {
+      return (
+        <View
+          style={[
+            a.p_lg,
+            index !== 0 && a.border_t,
+            t.atoms.border_contrast_low,
+            t.atoms.bg_contrast_25,
+          ]}>
+          <Text style={[a.font_bold, a.text_md, t.atoms.text_contrast_medium]}>
+            <Trans>Blocked post.</Trans>
+          </Text>
+        </View>
+      )
+    } else if (isThreadPost(item)) {
+      const prev = isThreadPost(posts[index - 1])
+        ? (posts[index - 1] as ThreadPost)
+        : undefined
+      const next = isThreadPost(posts[index + 1])
+        ? (posts[index + 1] as ThreadPost)
+        : undefined
+      const showChildReplyLine = (next?.ctx.depth || 0) > item.ctx.depth
+      const showParentReplyLine =
+        (item.ctx.depth < 0 && !!item.parent) || item.ctx.depth > 1
+      const hasUnrevealedParents =
+        index === 0 && skeleton?.parents && maxParents < skeleton.parents.length
+      return (
+        <View
+          ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}
+          onLayout={deferParents ? () => setDeferParents(false) : undefined}>
+          <PostThreadItem
+            post={item.post}
+            record={item.record}
+            moderation={threadModerationCache.get(item)}
+            treeView={treeView}
+            depth={item.ctx.depth}
+            prevPost={prev}
+            nextPost={next}
+            isHighlightedPost={item.ctx.isHighlightedPost}
+            hasMore={item.ctx.hasMore}
+            showChildReplyLine={showChildReplyLine}
+            showParentReplyLine={showParentReplyLine}
+            hasPrecedingItem={showParentReplyLine || !!hasUnrevealedParents}
+            overrideBlur={
+              hiddenRepliesState ===
+                HiddenRepliesState.ShowAndOverridePostHider &&
+              item.ctx.depth > 0
             }
+            onPostReply={refetch}
+            hideTopBorder={index === 0 && !item.ctx.isParentLoading}
           />
-        )
-      } else if (isThreadNotFound(item)) {
-        return (
-          <View style={[pal.border, pal.viewLight, styles.itemContainer]}>
-            <Text type="lg-bold" style={pal.textLight}>
-              <Trans>Deleted post.</Trans>
-            </Text>
-          </View>
-        )
-      } else if (isThreadBlocked(item)) {
-        return (
-          <View style={[pal.border, pal.viewLight, styles.itemContainer]}>
-            <Text type="lg-bold" style={pal.textLight}>
-              <Trans>Blocked post.</Trans>
-            </Text>
-          </View>
-        )
-      } else if (isThreadPost(item)) {
-        const prev = isThreadPost(posts[index - 1])
-          ? (posts[index - 1] as ThreadPost)
-          : undefined
-        const next = isThreadPost(posts[index + 1])
-          ? (posts[index + 1] as ThreadPost)
-          : undefined
-        const showChildReplyLine = (next?.ctx.depth || 0) > item.ctx.depth
-        const showParentReplyLine =
-          (item.ctx.depth < 0 && !!item.parent) || item.ctx.depth > 1
-        const hasUnrevealedParents =
-          index === 0 &&
-          skeleton?.parents &&
-          maxParents < skeleton.parents.length
-        return (
-          <View
-            ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}
-            onLayout={deferParents ? () => setDeferParents(false) : undefined}>
-            <PostThreadItem
-              post={item.post}
-              record={item.record}
-              moderation={threadModerationCache.get(item)}
-              treeView={treeView}
-              depth={item.ctx.depth}
-              prevPost={prev}
-              nextPost={next}
-              isHighlightedPost={item.ctx.isHighlightedPost}
-              hasMore={item.ctx.hasMore}
-              showChildReplyLine={showChildReplyLine}
-              showParentReplyLine={showParentReplyLine}
-              hasPrecedingItem={showParentReplyLine || !!hasUnrevealedParents}
-              overrideBlur={
-                hiddenRepliesState ===
-                  HiddenRepliesState.ShowAndOverridePostHider &&
-                item.ctx.depth > 0
-              }
-              onPostReply={refetch}
-            />
-          </View>
-        )
-      }
-      return null
-    },
-    [
-      hasSession,
-      isTabletOrMobile,
-      _,
-      isMobile,
-      onPressReply,
-      pal.border,
-      pal.viewLight,
-      pal.textLight,
-      posts,
-      skeleton?.parents,
-      maxParents,
-      deferParents,
-      treeView,
-      refetch,
-      threadModerationCache,
-      hiddenRepliesState,
-      setHiddenRepliesState,
-    ],
-  )
+        </View>
+      )
+    }
+    return null
+  }
 
   if (!thread || !preferences || error) {
     return (
@@ -449,39 +420,49 @@ export function PostThread({
   }
 
   return (
-    <ScrollProvider onMomentumEnd={onMomentumEnd}>
-      <List
-        ref={ref}
-        data={posts}
-        renderItem={renderItem}
-        keyExtractor={keyExtractor}
-        onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb}
-        onStartReached={onStartReached}
-        onEndReached={onEndReached}
-        onEndReachedThreshold={2}
-        onScrollToTop={onScrollToTop}
-        maintainVisibleContentPosition={
-          isNative ? MAINTAIN_VISIBLE_CONTENT_POSITION : undefined
-        }
-        // @ts-ignore our .web version only -prf
-        desktopFixedHeight
-        removeClippedSubviews={isAndroid ? false : undefined}
-        ListFooterComponent={
-          <ListFooter
-            // Using `isFetching` over `isFetchingNextPage` is done on purpose here so we get the loader on
-            // initial render
-            isFetchingNextPage={isFetching}
-            error={cleanError(threadError)}
-            onRetry={refetch}
-            // 300 is based on the minimum height of a post. This is enough extra height for the `maintainVisPos` to
-            // work without causing weird jumps on web or glitches on native
-            height={windowHeight - 200}
-          />
-        }
-        initialNumToRender={initialNumToRender}
-        windowSize={11}
-      />
-    </ScrollProvider>
+    <CenteredView style={[a.flex_1]} sideBorders={true}>
+      {showHeader && (
+        <ViewHeader
+          title={_(msg({message: `Post`, context: 'description'}))}
+          showBorder
+        />
+      )}
+
+      <ScrollProvider onMomentumEnd={onMomentumEnd}>
+        <List
+          ref={ref}
+          data={posts}
+          renderItem={renderItem}
+          keyExtractor={keyExtractor}
+          onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb}
+          onStartReached={onStartReached}
+          onEndReached={onEndReached}
+          onEndReachedThreshold={2}
+          onScrollToTop={onScrollToTop}
+          maintainVisibleContentPosition={
+            isNative ? MAINTAIN_VISIBLE_CONTENT_POSITION : undefined
+          }
+          // @ts-ignore our .web version only -prf
+          desktopFixedHeight
+          removeClippedSubviews={isAndroid ? false : undefined}
+          ListFooterComponent={
+            <ListFooter
+              // Using `isFetching` over `isFetchingNextPage` is done on purpose here so we get the loader on
+              // initial render
+              isFetchingNextPage={isFetching}
+              error={cleanError(threadError)}
+              onRetry={refetch}
+              // 300 is based on the minimum height of a post. This is enough extra height for the `maintainVisPos` to
+              // work without causing weird jumps on web or glitches on native
+              height={windowHeight - 200}
+            />
+          }
+          initialNumToRender={initialNumToRender}
+          windowSize={11}
+          sideBorders={false}
+        />
+      </ScrollProvider>
+    </CenteredView>
   )
 }
 
@@ -630,11 +611,3 @@ function hasBranchingReplies(node?: ThreadNode) {
   }
   return true
 }
-
-const styles = StyleSheet.create({
-  itemContainer: {
-    borderTopWidth: 1,
-    paddingHorizontal: 18,
-    paddingVertical: 18,
-  },
-})
-- 
cgit 1.4.1

