diff options
author | Hailey <me@haileyok.com> | 2024-05-29 20:28:32 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-05-30 04:28:32 +0100 |
commit | 9edb4879494b348616caca6999ee89658f439c49 (patch) | |
tree | 276f24d9b2cefb6fdde8ef5964c650c21037a41c | |
parent | 9628070e52c4f50e2f381a3f4ad1f3932743d011 (diff) | |
download | voidsky-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.tsx | 337 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 43 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThreadShowHiddenReplies.tsx | 4 |
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, ]}> |