Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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>

authored by

Hailey
Dan Abramov
and committed by
GitHub
9edb4879 9628070e

+194 -190
+155 -182
src/view/com/post-thread/PostThread.tsx
··· 1 1 import React, {useEffect, useRef} from 'react' 2 - import {StyleSheet, useWindowDimensions, View} from 'react-native' 2 + import {useWindowDimensions, View} from 'react-native' 3 3 import {runOnJS} from 'react-native-reanimated' 4 4 import {AppBskyFeedDefs} from '@atproto/api' 5 5 import {msg, Trans} from '@lingui/macro' ··· 22 22 import {usePreferencesQuery} from '#/state/queries/preferences' 23 23 import {useSession} from '#/state/session' 24 24 import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender' 25 - import {usePalette} from 'lib/hooks/usePalette' 26 25 import {useSetTitle} from 'lib/hooks/useSetTitle' 27 26 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 28 27 import {sanitizeDisplayName} from 'lib/strings/display-names' 29 28 import {cleanError} from 'lib/strings/errors' 29 + import {CenteredView} from 'view/com/util/Views' 30 + import {atoms as a, useTheme} from '#/alf' 30 31 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 32 + import {Text} from '#/components/Typography' 31 33 import {ComposePrompt} from '../composer/Prompt' 32 34 import {List, ListMethods} from '../util/List' 33 - import {Text} from '../util/text/Text' 34 35 import {ViewHeader} from '../util/ViewHeader' 35 36 import {PostThreadItem} from './PostThreadItem' 36 37 import {PostThreadShowHiddenReplies} from './PostThreadShowHiddenReplies' ··· 45 46 minIndexForVisible: 0, 46 47 } 47 48 48 - const TOP_COMPONENT = {_reactKey: '__top_component__'} 49 49 const REPLY_PROMPT = {_reactKey: '__reply__'} 50 50 const LOAD_MORE = {_reactKey: '__load_more__'} 51 51 const SHOW_HIDDEN_REPLIES = {_reactKey: '__show_hidden_replies__'} ··· 66 66 type RowItem = 67 67 | YieldedItem 68 68 // TODO: TS doesn't actually enforce it's one of these, it only enforces matching shape. 69 - | typeof TOP_COMPONENT 70 69 | typeof REPLY_PROMPT 71 70 | typeof LOAD_MORE 72 71 ··· 91 90 }) { 92 91 const {hasSession} = useSession() 93 92 const {_} = useLingui() 94 - const pal = usePalette('default') 93 + const t = useTheme() 95 94 const {isMobile, isTabletOrMobile} = useWebMediaQueries() 96 95 const initialNumToRender = useInitialNumToRender() 97 96 const {height: windowHeight} = useWindowDimensions() ··· 224 223 const {parents, highlightedPost, replies} = skeleton 225 224 let arr: RowItem[] = [] 226 225 if (highlightedPost.type === 'post') { 227 - const isRoot = 228 - !highlightedPost.parent && !highlightedPost.ctx.isParentLoading 229 - if (isRoot) { 230 - // No parents to load. 231 - arr.push(TOP_COMPONENT) 232 - } else { 233 - if (highlightedPost.ctx.isParentLoading || deferParents) { 234 - // We're loading parents of the highlighted post. 235 - // In this case, we don't render anything above the post. 236 - // If you add something here, you'll need to update both 237 - // maintainVisibleContentPosition and onContentSizeChange 238 - // to "hold onto" the correct row instead of the first one. 239 - } else { 240 - // Everything is loaded 241 - let startIndex = Math.max(0, parents.length - maxParents) 242 - if (startIndex === 0) { 243 - arr.push(TOP_COMPONENT) 244 - } else { 245 - // When progressively revealing parents, rendering a placeholder 246 - // here will cause scrolling jumps. Don't add it unless you test it. 247 - // QT'ing this thread is a great way to test all the scrolling hacks: 248 - // https://bsky.app/profile/www.mozzius.dev/post/3kjqhblh6qk2o 249 - } 250 - for (let i = startIndex; i < parents.length; i++) { 251 - arr.push(parents[i]) 252 - } 226 + // We want to wait for parents to load before rendering. 227 + // If you add something here, you'll need to update both 228 + // maintainVisibleContentPosition and onContentSizeChange 229 + // to "hold onto" the correct row instead of the first one. 230 + 231 + if (!highlightedPost.ctx.isParentLoading && !deferParents) { 232 + // When progressively revealing parents, rendering a placeholder 233 + // here will cause scrolling jumps. Don't add it unless you test it. 234 + // QT'ing this thread is a great way to test all the scrolling hacks: 235 + // https://bsky.app/profile/www.mozzius.dev/post/3kjqhblh6qk2o 236 + 237 + // Everything is loaded 238 + let startIndex = Math.max(0, parents.length - maxParents) 239 + for (let i = startIndex; i < parents.length; i++) { 240 + arr.push(parents[i]) 253 241 } 254 242 } 255 243 arr.push(highlightedPost) ··· 323 311 setMaxReplies(prev => prev + 50) 324 312 }, [isFetching, maxReplies, posts.length]) 325 313 326 - const renderItem = React.useCallback( 327 - ({item, index}: {item: RowItem; index: number}) => { 328 - if (item === TOP_COMPONENT) { 329 - return isTabletOrMobile ? ( 330 - <ViewHeader 331 - title={_(msg({message: `Post`, context: 'description'}))} 332 - /> 333 - ) : null 334 - } else if (item === REPLY_PROMPT && hasSession) { 335 - return ( 336 - <View> 337 - {!isMobile && <ComposePrompt onPressCompose={onPressReply} />} 338 - </View> 339 - ) 340 - } else if (item === SHOW_HIDDEN_REPLIES) { 341 - return ( 342 - <PostThreadShowHiddenReplies 343 - type="hidden" 344 - onPress={() => 345 - setHiddenRepliesState(HiddenRepliesState.ShowAndOverridePostHider) 314 + const hasParents = 315 + skeleton?.highlightedPost?.type === 'post' && 316 + (skeleton.highlightedPost.ctx.isParentLoading || 317 + Boolean(skeleton?.parents && skeleton.parents.length > 0)) 318 + const showHeader = 319 + isNative || (isTabletOrMobile && (!hasParents || !isFetching)) 320 + 321 + const renderItem = ({item, index}: {item: RowItem; index: number}) => { 322 + if (item === REPLY_PROMPT && hasSession) { 323 + return ( 324 + <View> 325 + {!isMobile && <ComposePrompt onPressCompose={onPressReply} />} 326 + </View> 327 + ) 328 + } else if (item === SHOW_HIDDEN_REPLIES || item === SHOW_MUTED_REPLIES) { 329 + return ( 330 + <PostThreadShowHiddenReplies 331 + type={item === SHOW_HIDDEN_REPLIES ? 'hidden' : 'muted'} 332 + onPress={() => 333 + setHiddenRepliesState(HiddenRepliesState.ShowAndOverridePostHider) 334 + } 335 + hideTopBorder={index === 0} 336 + /> 337 + ) 338 + } else if (isThreadNotFound(item)) { 339 + return ( 340 + <View 341 + style={[ 342 + a.p_lg, 343 + index !== 0 && a.border_t, 344 + t.atoms.border_contrast_low, 345 + t.atoms.bg_contrast_25, 346 + ]}> 347 + <Text style={[a.font_bold, a.text_md, t.atoms.text_contrast_medium]}> 348 + <Trans>Deleted post.</Trans> 349 + </Text> 350 + </View> 351 + ) 352 + } else if (isThreadBlocked(item)) { 353 + return ( 354 + <View 355 + style={[ 356 + a.p_lg, 357 + index !== 0 && a.border_t, 358 + t.atoms.border_contrast_low, 359 + t.atoms.bg_contrast_25, 360 + ]}> 361 + <Text style={[a.font_bold, a.text_md, t.atoms.text_contrast_medium]}> 362 + <Trans>Blocked post.</Trans> 363 + </Text> 364 + </View> 365 + ) 366 + } else if (isThreadPost(item)) { 367 + const prev = isThreadPost(posts[index - 1]) 368 + ? (posts[index - 1] as ThreadPost) 369 + : undefined 370 + const next = isThreadPost(posts[index + 1]) 371 + ? (posts[index + 1] as ThreadPost) 372 + : undefined 373 + const showChildReplyLine = (next?.ctx.depth || 0) > item.ctx.depth 374 + const showParentReplyLine = 375 + (item.ctx.depth < 0 && !!item.parent) || item.ctx.depth > 1 376 + const hasUnrevealedParents = 377 + index === 0 && skeleton?.parents && maxParents < skeleton.parents.length 378 + return ( 379 + <View 380 + ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined} 381 + onLayout={deferParents ? () => setDeferParents(false) : undefined}> 382 + <PostThreadItem 383 + post={item.post} 384 + record={item.record} 385 + moderation={threadModerationCache.get(item)} 386 + treeView={treeView} 387 + depth={item.ctx.depth} 388 + prevPost={prev} 389 + nextPost={next} 390 + isHighlightedPost={item.ctx.isHighlightedPost} 391 + hasMore={item.ctx.hasMore} 392 + showChildReplyLine={showChildReplyLine} 393 + showParentReplyLine={showParentReplyLine} 394 + hasPrecedingItem={showParentReplyLine || !!hasUnrevealedParents} 395 + overrideBlur={ 396 + hiddenRepliesState === 397 + HiddenRepliesState.ShowAndOverridePostHider && 398 + item.ctx.depth > 0 346 399 } 400 + onPostReply={refetch} 401 + hideTopBorder={index === 0 && !item.ctx.isParentLoading} 347 402 /> 348 - ) 349 - } else if (item === SHOW_MUTED_REPLIES) { 350 - return ( 351 - <PostThreadShowHiddenReplies 352 - type="muted" 353 - onPress={() => 354 - setHiddenRepliesState(HiddenRepliesState.ShowAndOverridePostHider) 355 - } 356 - /> 357 - ) 358 - } else if (isThreadNotFound(item)) { 359 - return ( 360 - <View style={[pal.border, pal.viewLight, styles.itemContainer]}> 361 - <Text type="lg-bold" style={pal.textLight}> 362 - <Trans>Deleted post.</Trans> 363 - </Text> 364 - </View> 365 - ) 366 - } else if (isThreadBlocked(item)) { 367 - return ( 368 - <View style={[pal.border, pal.viewLight, styles.itemContainer]}> 369 - <Text type="lg-bold" style={pal.textLight}> 370 - <Trans>Blocked post.</Trans> 371 - </Text> 372 - </View> 373 - ) 374 - } else if (isThreadPost(item)) { 375 - const prev = isThreadPost(posts[index - 1]) 376 - ? (posts[index - 1] as ThreadPost) 377 - : undefined 378 - const next = isThreadPost(posts[index + 1]) 379 - ? (posts[index + 1] as ThreadPost) 380 - : undefined 381 - const showChildReplyLine = (next?.ctx.depth || 0) > item.ctx.depth 382 - const showParentReplyLine = 383 - (item.ctx.depth < 0 && !!item.parent) || item.ctx.depth > 1 384 - const hasUnrevealedParents = 385 - index === 0 && 386 - skeleton?.parents && 387 - maxParents < skeleton.parents.length 388 - return ( 389 - <View 390 - ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined} 391 - onLayout={deferParents ? () => setDeferParents(false) : undefined}> 392 - <PostThreadItem 393 - post={item.post} 394 - record={item.record} 395 - moderation={threadModerationCache.get(item)} 396 - treeView={treeView} 397 - depth={item.ctx.depth} 398 - prevPost={prev} 399 - nextPost={next} 400 - isHighlightedPost={item.ctx.isHighlightedPost} 401 - hasMore={item.ctx.hasMore} 402 - showChildReplyLine={showChildReplyLine} 403 - showParentReplyLine={showParentReplyLine} 404 - hasPrecedingItem={showParentReplyLine || !!hasUnrevealedParents} 405 - overrideBlur={ 406 - hiddenRepliesState === 407 - HiddenRepliesState.ShowAndOverridePostHider && 408 - item.ctx.depth > 0 409 - } 410 - onPostReply={refetch} 411 - /> 412 - </View> 413 - ) 414 - } 415 - return null 416 - }, 417 - [ 418 - hasSession, 419 - isTabletOrMobile, 420 - _, 421 - isMobile, 422 - onPressReply, 423 - pal.border, 424 - pal.viewLight, 425 - pal.textLight, 426 - posts, 427 - skeleton?.parents, 428 - maxParents, 429 - deferParents, 430 - treeView, 431 - refetch, 432 - threadModerationCache, 433 - hiddenRepliesState, 434 - setHiddenRepliesState, 435 - ], 436 - ) 403 + </View> 404 + ) 405 + } 406 + return null 407 + } 437 408 438 409 if (!thread || !preferences || error) { 439 410 return ( ··· 449 420 } 450 421 451 422 return ( 452 - <ScrollProvider onMomentumEnd={onMomentumEnd}> 453 - <List 454 - ref={ref} 455 - data={posts} 456 - renderItem={renderItem} 457 - keyExtractor={keyExtractor} 458 - onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb} 459 - onStartReached={onStartReached} 460 - onEndReached={onEndReached} 461 - onEndReachedThreshold={2} 462 - onScrollToTop={onScrollToTop} 463 - maintainVisibleContentPosition={ 464 - isNative ? MAINTAIN_VISIBLE_CONTENT_POSITION : undefined 465 - } 466 - // @ts-ignore our .web version only -prf 467 - desktopFixedHeight 468 - removeClippedSubviews={isAndroid ? false : undefined} 469 - ListFooterComponent={ 470 - <ListFooter 471 - // Using `isFetching` over `isFetchingNextPage` is done on purpose here so we get the loader on 472 - // initial render 473 - isFetchingNextPage={isFetching} 474 - error={cleanError(threadError)} 475 - onRetry={refetch} 476 - // 300 is based on the minimum height of a post. This is enough extra height for the `maintainVisPos` to 477 - // work without causing weird jumps on web or glitches on native 478 - height={windowHeight - 200} 479 - /> 480 - } 481 - initialNumToRender={initialNumToRender} 482 - windowSize={11} 483 - /> 484 - </ScrollProvider> 423 + <CenteredView style={[a.flex_1]} sideBorders={true}> 424 + {showHeader && ( 425 + <ViewHeader 426 + title={_(msg({message: `Post`, context: 'description'}))} 427 + showBorder 428 + /> 429 + )} 430 + 431 + <ScrollProvider onMomentumEnd={onMomentumEnd}> 432 + <List 433 + ref={ref} 434 + data={posts} 435 + renderItem={renderItem} 436 + keyExtractor={keyExtractor} 437 + onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb} 438 + onStartReached={onStartReached} 439 + onEndReached={onEndReached} 440 + onEndReachedThreshold={2} 441 + onScrollToTop={onScrollToTop} 442 + maintainVisibleContentPosition={ 443 + isNative ? MAINTAIN_VISIBLE_CONTENT_POSITION : undefined 444 + } 445 + // @ts-ignore our .web version only -prf 446 + desktopFixedHeight 447 + removeClippedSubviews={isAndroid ? false : undefined} 448 + ListFooterComponent={ 449 + <ListFooter 450 + // Using `isFetching` over `isFetchingNextPage` is done on purpose here so we get the loader on 451 + // initial render 452 + isFetchingNextPage={isFetching} 453 + error={cleanError(threadError)} 454 + onRetry={refetch} 455 + // 300 is based on the minimum height of a post. This is enough extra height for the `maintainVisPos` to 456 + // work without causing weird jumps on web or glitches on native 457 + height={windowHeight - 200} 458 + /> 459 + } 460 + initialNumToRender={initialNumToRender} 461 + windowSize={11} 462 + sideBorders={false} 463 + /> 464 + </ScrollProvider> 465 + </CenteredView> 485 466 ) 486 467 } 487 468 ··· 630 611 } 631 612 return true 632 613 } 633 - 634 - const styles = StyleSheet.create({ 635 - itemContainer: { 636 - borderTopWidth: 1, 637 - paddingHorizontal: 18, 638 - paddingVertical: 18, 639 - }, 640 - })
+36 -7
src/view/com/post-thread/PostThreadItem.tsx
··· 65 65 hasPrecedingItem, 66 66 overrideBlur, 67 67 onPostReply, 68 + hideTopBorder, 68 69 }: { 69 70 post: AppBskyFeedDefs.PostView 70 71 record: AppBskyFeedPost.Record ··· 80 81 hasPrecedingItem: boolean 81 82 overrideBlur: boolean 82 83 onPostReply: () => void 84 + hideTopBorder?: boolean 83 85 }) { 84 86 const postShadowed = usePostShadow(post) 85 87 const richText = useMemo( ··· 91 93 [record], 92 94 ) 93 95 if (postShadowed === POST_TOMBSTONE) { 94 - return <PostThreadItemDeleted /> 96 + return <PostThreadItemDeleted hideTopBorder={hideTopBorder} /> 95 97 } 96 98 if (richText && moderation) { 97 99 return ( ··· 113 115 hasPrecedingItem={hasPrecedingItem} 114 116 overrideBlur={overrideBlur} 115 117 onPostReply={onPostReply} 118 + hideTopBorder={hideTopBorder} 116 119 /> 117 120 ) 118 121 } 119 122 return null 120 123 } 121 124 122 - function PostThreadItemDeleted() { 125 + function PostThreadItemDeleted({hideTopBorder}: {hideTopBorder?: boolean}) { 123 126 const pal = usePalette('default') 124 127 return ( 125 - <View style={[styles.outer, pal.border, pal.view, s.p20, s.flexRow]}> 128 + <View 129 + style={[ 130 + styles.outer, 131 + pal.border, 132 + pal.view, 133 + s.p20, 134 + s.flexRow, 135 + hideTopBorder && styles.noTopBorder, 136 + ]}> 126 137 <FontAwesomeIcon icon={['far', 'trash-can']} color={pal.colors.icon} /> 127 138 <Text style={[pal.textLight, s.ml10]}> 128 139 <Trans>This post has been deleted.</Trans> ··· 147 158 hasPrecedingItem, 148 159 overrideBlur, 149 160 onPostReply, 161 + hideTopBorder, 150 162 }: { 151 163 post: Shadow<AppBskyFeedDefs.PostView> 152 164 record: AppBskyFeedPost.Record ··· 163 175 hasPrecedingItem: boolean 164 176 overrideBlur: boolean 165 177 onPostReply: () => void 178 + hideTopBorder?: boolean 166 179 }): React.ReactNode => { 167 180 const pal = usePalette('default') 168 181 const {_} = useLingui() ··· 237 250 styles.replyLine, 238 251 { 239 252 flexGrow: 1, 240 - backgroundColor: pal.colors.border, 253 + backgroundColor: pal.colors.replyLine, 241 254 }, 242 255 ]} 243 256 /> ··· 247 260 248 261 <View 249 262 testID={`postThreadItem-by-${post.author.handle}`} 250 - style={[styles.outer, styles.outerHighlighted, pal.border, pal.view]} 263 + style={[ 264 + styles.outer, 265 + styles.outerHighlighted, 266 + pal.border, 267 + pal.view, 268 + rootUri === post.uri && styles.outerHighlightedRoot, 269 + hideTopBorder && styles.noTopBorder, 270 + ]} 251 271 accessible={false}> 252 272 <View style={[styles.layout]}> 253 273 <View style={[styles.layoutAvi, {paddingBottom: 8}]}> ··· 395 415 depth={depth} 396 416 showParentReplyLine={!!showParentReplyLine} 397 417 treeView={treeView} 398 - hasPrecedingItem={hasPrecedingItem}> 418 + hasPrecedingItem={hasPrecedingItem} 419 + hideTopBorder={hideTopBorder}> 399 420 <PostHider 400 421 testID={`postThreadItem-by-${post.author.handle}`} 401 422 href={postHref} ··· 574 595 depth, 575 596 showParentReplyLine, 576 597 hasPrecedingItem, 598 + hideTopBorder, 577 599 children, 578 600 }: React.PropsWithChildren<{ 579 601 post: AppBskyFeedDefs.PostView ··· 581 603 depth: number 582 604 showParentReplyLine: boolean 583 605 hasPrecedingItem: boolean 606 + hideTopBorder?: boolean 584 607 }>) { 585 608 const {isMobile} = useWebMediaQueries() 586 609 const pal = usePalette('default') ··· 617 640 styles.outer, 618 641 pal.border, 619 642 showParentReplyLine && hasPrecedingItem && styles.noTopBorder, 643 + hideTopBorder && styles.noTopBorder, 620 644 styles.cursor, 621 645 ]}> 622 646 {children} ··· 677 701 paddingLeft: 8, 678 702 }, 679 703 outerHighlighted: { 680 - paddingTop: 16, 704 + borderTopWidth: 0, 705 + paddingTop: 4, 681 706 paddingLeft: 8, 682 707 paddingRight: 8, 708 + }, 709 + outerHighlightedRoot: { 710 + borderTopWidth: 1, 711 + paddingTop: 16, 683 712 }, 684 713 noTopBorder: { 685 714 borderTopWidth: 0,
+3 -1
src/view/com/post-thread/PostThreadShowHiddenReplies.tsx
··· 11 11 export function PostThreadShowHiddenReplies({ 12 12 type, 13 13 onPress, 14 + hideTopBorder, 14 15 }: { 15 16 type: 'hidden' | 'muted' 16 17 onPress: () => void 18 + hideTopBorder?: boolean 17 19 }) { 18 20 const {_} = useLingui() 19 21 const t = useTheme() ··· 31 33 a.gap_sm, 32 34 a.py_lg, 33 35 a.px_xl, 34 - a.border_t, 36 + !hideTopBorder && a.border_t, 35 37 t.atoms.border_contrast_low, 36 38 hovered || pressed ? t.atoms.bg_contrast_25 : t.atoms.bg, 37 39 ]}>