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>
zio/stable
Hailey 2024-05-29 20:28:32 -07:00 committed by GitHub
parent 9628070e52
commit 9edb487949
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 194 additions and 190 deletions

View File

@ -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)
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 (item === SHOW_MUTED_REPLIES) {
return (
<PostThreadShowHiddenReplies
type="muted"
onPress={() =>
setHiddenRepliesState(HiddenRepliesState.ShowAndOverridePostHider)
}
/>
)
} 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,
},
})

View File

@ -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,
},

View File

@ -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,
]}>