diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index 4f7d0d3c..1212f992 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 ? ( - - ) : null - } else if (item === REPLY_PROMPT && hasSession) { - return ( - - {!isMobile && } - - ) - } else if (item === SHOW_HIDDEN_REPLIES) { - return ( - - 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 ( + + {!isMobile && } + + ) + } else if (item === SHOW_HIDDEN_REPLIES || item === SHOW_MUTED_REPLIES) { + return ( + + setHiddenRepliesState(HiddenRepliesState.ShowAndOverridePostHider) + } + hideTopBorder={index === 0} + /> + ) + } else if (isThreadNotFound(item)) { + return ( + + + Deleted post. + + + ) + } else if (isThreadBlocked(item)) { + return ( + + + Blocked post. + + + ) + } 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 ( + setDeferParents(false) : undefined}> + 0 } + onPostReply={refetch} + hideTopBorder={index === 0 && !item.ctx.isParentLoading} /> - ) - } else if (item === SHOW_MUTED_REPLIES) { - return ( - - setHiddenRepliesState(HiddenRepliesState.ShowAndOverridePostHider) - } - /> - ) - } else if (isThreadNotFound(item)) { - return ( - - - Deleted post. - - - ) - } else if (isThreadBlocked(item)) { - return ( - - - Blocked post. - - - ) - } 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 ( - setDeferParents(false) : undefined}> - 0 - } - onPostReply={refetch} - /> - - ) - } - return null - }, - [ - hasSession, - isTabletOrMobile, - _, - isMobile, - onPressReply, - pal.border, - pal.viewLight, - pal.textLight, - posts, - skeleton?.parents, - maxParents, - deferParents, - treeView, - refetch, - threadModerationCache, - hiddenRepliesState, - setHiddenRepliesState, - ], - ) + + ) + } + return null + } if (!thread || !preferences || error) { return ( @@ -449,39 +420,49 @@ export function PostThread({ } return ( - - - } - initialNumToRender={initialNumToRender} - windowSize={11} - /> - + + {showHeader && ( + + )} + + + + } + initialNumToRender={initialNumToRender} + windowSize={11} + sideBorders={false} + /> + + ) } @@ -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 0ff040b9..99fbda6d 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 + return } 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 ( - + This post has been deleted. @@ -147,6 +158,7 @@ let PostThreadItemLoaded = ({ hasPrecedingItem, overrideBlur, onPostReply, + hideTopBorder, }: { post: Shadow 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 = ({ @@ -395,7 +415,8 @@ let PostThreadItemLoaded = ({ depth={depth} showParentReplyLine={!!showParentReplyLine} treeView={treeView} - hasPrecedingItem={hasPrecedingItem}> + hasPrecedingItem={hasPrecedingItem} + hideTopBorder={hideTopBorder}> ) { 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 99890652..7c021d88 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, ]}>