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>
This commit is contained in:
parent
9628070e52
commit
9edb487949
3 changed files with 194 additions and 190 deletions
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue