about summary refs log tree commit diff
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-05-29 20:28:32 -0700
committerGitHub <noreply@github.com>2024-05-30 04:28:32 +0100
commit9edb4879494b348616caca6999ee89658f439c49 (patch)
tree276f24d9b2cefb6fdde8ef5964c650c21037a41c
parent9628070e52c4f50e2f381a3f4ad1f3932743d011 (diff)
downloadvoidsky-9edb4879494b348616caca6999ee89658f439c49.tar.zst
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>
-rw-r--r--src/view/com/post-thread/PostThread.tsx337
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx43
-rw-r--r--src/view/com/post-thread/PostThreadShowHiddenReplies.tsx4
3 files changed, 194 insertions, 190 deletions
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,
-  },
-})
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 0ff040b9c..99fbda6d2 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -65,6 +65,7 @@ export function PostThreadItem({
   hasPrecedingItem,
   overrideBlur,
   onPostReply,
+  hideTopBorder,
 }: {
   post: AppBskyFeedDefs.PostView
   record: AppBskyFeedPost.Record
@@ -80,6 +81,7 @@ export function PostThreadItem({
   hasPrecedingItem: boolean
   overrideBlur: boolean
   onPostReply: () => void
+  hideTopBorder?: boolean
 }) {
   const postShadowed = usePostShadow(post)
   const richText = useMemo(
@@ -91,7 +93,7 @@ export function PostThreadItem({
     [record],
   )
   if (postShadowed === POST_TOMBSTONE) {
-    return <PostThreadItemDeleted />
+    return <PostThreadItemDeleted hideTopBorder={hideTopBorder} />
   }
   if (richText && moderation) {
     return (
@@ -113,16 +115,25 @@ export function PostThreadItem({
         hasPrecedingItem={hasPrecedingItem}
         overrideBlur={overrideBlur}
         onPostReply={onPostReply}
+        hideTopBorder={hideTopBorder}
       />
     )
   }
   return null
 }
 
-function PostThreadItemDeleted() {
+function PostThreadItemDeleted({hideTopBorder}: {hideTopBorder?: boolean}) {
   const pal = usePalette('default')
   return (
-    <View style={[styles.outer, pal.border, pal.view, s.p20, s.flexRow]}>
+    <View
+      style={[
+        styles.outer,
+        pal.border,
+        pal.view,
+        s.p20,
+        s.flexRow,
+        hideTopBorder && styles.noTopBorder,
+      ]}>
       <FontAwesomeIcon icon={['far', 'trash-can']} color={pal.colors.icon} />
       <Text style={[pal.textLight, s.ml10]}>
         <Trans>This post has been deleted.</Trans>
@@ -147,6 +158,7 @@ let PostThreadItemLoaded = ({
   hasPrecedingItem,
   overrideBlur,
   onPostReply,
+  hideTopBorder,
 }: {
   post: Shadow<AppBskyFeedDefs.PostView>
   record: AppBskyFeedPost.Record
@@ -163,6 +175,7 @@ let PostThreadItemLoaded = ({
   hasPrecedingItem: boolean
   overrideBlur: boolean
   onPostReply: () => void
+  hideTopBorder?: boolean
 }): React.ReactNode => {
   const pal = usePalette('default')
   const {_} = useLingui()
@@ -237,7 +250,7 @@ let PostThreadItemLoaded = ({
                   styles.replyLine,
                   {
                     flexGrow: 1,
-                    backgroundColor: pal.colors.border,
+                    backgroundColor: pal.colors.replyLine,
                   },
                 ]}
               />
@@ -247,7 +260,14 @@ let PostThreadItemLoaded = ({
 
         <View
           testID={`postThreadItem-by-${post.author.handle}`}
-          style={[styles.outer, styles.outerHighlighted, pal.border, pal.view]}
+          style={[
+            styles.outer,
+            styles.outerHighlighted,
+            pal.border,
+            pal.view,
+            rootUri === post.uri && styles.outerHighlightedRoot,
+            hideTopBorder && styles.noTopBorder,
+          ]}
           accessible={false}>
           <View style={[styles.layout]}>
             <View style={[styles.layoutAvi, {paddingBottom: 8}]}>
@@ -395,7 +415,8 @@ let PostThreadItemLoaded = ({
           depth={depth}
           showParentReplyLine={!!showParentReplyLine}
           treeView={treeView}
-          hasPrecedingItem={hasPrecedingItem}>
+          hasPrecedingItem={hasPrecedingItem}
+          hideTopBorder={hideTopBorder}>
           <PostHider
             testID={`postThreadItem-by-${post.author.handle}`}
             href={postHref}
@@ -574,6 +595,7 @@ function PostOuterWrapper({
   depth,
   showParentReplyLine,
   hasPrecedingItem,
+  hideTopBorder,
   children,
 }: React.PropsWithChildren<{
   post: AppBskyFeedDefs.PostView
@@ -581,6 +603,7 @@ function PostOuterWrapper({
   depth: number
   showParentReplyLine: boolean
   hasPrecedingItem: boolean
+  hideTopBorder?: boolean
 }>) {
   const {isMobile} = useWebMediaQueries()
   const pal = usePalette('default')
@@ -617,6 +640,7 @@ function PostOuterWrapper({
         styles.outer,
         pal.border,
         showParentReplyLine && hasPrecedingItem && styles.noTopBorder,
+        hideTopBorder && styles.noTopBorder,
         styles.cursor,
       ]}>
       {children}
@@ -677,10 +701,15 @@ const styles = StyleSheet.create({
     paddingLeft: 8,
   },
   outerHighlighted: {
-    paddingTop: 16,
+    borderTopWidth: 0,
+    paddingTop: 4,
     paddingLeft: 8,
     paddingRight: 8,
   },
+  outerHighlightedRoot: {
+    borderTopWidth: 1,
+    paddingTop: 16,
+  },
   noTopBorder: {
     borderTopWidth: 0,
   },
diff --git a/src/view/com/post-thread/PostThreadShowHiddenReplies.tsx b/src/view/com/post-thread/PostThreadShowHiddenReplies.tsx
index 998906524..7c021d88b 100644
--- a/src/view/com/post-thread/PostThreadShowHiddenReplies.tsx
+++ b/src/view/com/post-thread/PostThreadShowHiddenReplies.tsx
@@ -11,9 +11,11 @@ import {Text} from '#/components/Typography'
 export function PostThreadShowHiddenReplies({
   type,
   onPress,
+  hideTopBorder,
 }: {
   type: 'hidden' | 'muted'
   onPress: () => void
+  hideTopBorder?: boolean
 }) {
   const {_} = useLingui()
   const t = useTheme()
@@ -31,7 +33,7 @@ export function PostThreadShowHiddenReplies({
             a.gap_sm,
             a.py_lg,
             a.px_xl,
-            a.border_t,
+            !hideTopBorder && a.border_t,
             t.atoms.border_contrast_low,
             hovered || pressed ? t.atoms.bg_contrast_25 : t.atoms.bg,
           ]}>