Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

Tree view threads experiment (#1480)

* Add tree-view experiment to threads

* Fix typo

* Remove extra minimalshellmode call

* Fix to parent line rendering

* Fix extra border

* Some ui cleanup

authored by

Paul Frazee and committed by
GitHub
1af8e83d d2c253a2

+178 -53
+14 -1
src/state/models/ui/preferences.ts
··· 58 58 homeFeedMergeFeedEnabled: boolean = false 59 59 threadDefaultSort: string = 'oldest' 60 60 threadFollowedUsersFirst: boolean = true 61 + threadTreeViewEnabled: boolean = false 61 62 requireAltTextEnabled: boolean = false 62 63 63 64 // used to linearize async modifications to state ··· 91 92 homeFeedMergeFeedEnabled: this.homeFeedMergeFeedEnabled, 92 93 threadDefaultSort: this.threadDefaultSort, 93 94 threadFollowedUsersFirst: this.threadFollowedUsersFirst, 95 + threadTreeViewEnabled: this.threadTreeViewEnabled, 94 96 requireAltTextEnabled: this.requireAltTextEnabled, 95 97 } 96 98 } ··· 202 204 ) { 203 205 this.threadDefaultSort = v.threadDefaultSort 204 206 } 205 - // check if tread followed-users-first is enabled in preferences, then hydrate 207 + // check if thread followed-users-first is enabled in preferences, then hydrate 206 208 if ( 207 209 hasProp(v, 'threadFollowedUsersFirst') && 208 210 typeof v.threadFollowedUsersFirst === 'boolean' 209 211 ) { 210 212 this.threadFollowedUsersFirst = v.threadFollowedUsersFirst 213 + } 214 + // check if thread treeview is enabled in preferences, then hydrate 215 + if ( 216 + hasProp(v, 'threadTreeViewEnabled') && 217 + typeof v.threadTreeViewEnabled === 'boolean' 218 + ) { 219 + this.threadTreeViewEnabled = v.threadTreeViewEnabled 211 220 } 212 221 // check if requiring alt text is enabled in preferences, then hydrate 213 222 if ( ··· 522 531 523 532 toggleThreadFollowedUsersFirst() { 524 533 this.threadFollowedUsersFirst = !this.threadFollowedUsersFirst 534 + } 535 + 536 + toggleThreadTreeViewEnabled() { 537 + this.threadTreeViewEnabled = !this.threadTreeViewEnabled 525 538 } 526 539 527 540 toggleRequireAltTextEnabled() {
+36 -12
src/view/com/post-thread/PostThread.tsx
··· 55 55 const BOTTOM_COMPONENT = { 56 56 _reactKey: '__bottom_component__', 57 57 _isHighlightedPost: false, 58 + _showBorder: true, 58 59 } 59 60 type YieldedItem = 60 61 | PostThreadItemModel ··· 69 70 uri, 70 71 view, 71 72 onPressReply, 73 + treeView, 72 74 }: { 73 75 uri: string 74 76 view: PostThreadModel 75 77 onPressReply: () => void 78 + treeView: boolean 76 79 }) { 77 80 const pal = usePalette('default') 78 81 const {isTablet} = useWebMediaQueries() ··· 99 102 } 100 103 return [] 101 104 }, [view.isLoadingFromCache, view.thread, maxVisible]) 105 + const highlightedPostIndex = posts.findIndex(post => post._isHighlightedPost) 106 + const showBottomBorder = 107 + !treeView || 108 + // in the treeview, only show the bottom border 109 + // if there are replies under the highlighted posts 110 + posts.findLast(v => v instanceof PostThreadItemModel) !== 111 + posts[highlightedPostIndex] 102 112 useSetTitle( 103 113 view.thread?.postRecord && 104 114 `${sanitizeDisplayName( ··· 135 145 return 136 146 } 137 147 138 - const index = posts.findIndex(post => post._isHighlightedPost) 139 - if (index !== -1) { 148 + if (highlightedPostIndex !== -1) { 140 149 ref.current?.scrollToIndex({ 141 - index, 150 + index: highlightedPostIndex, 142 151 animated: false, 143 152 viewPosition: 0, 144 153 }) 145 154 hasScrolledIntoView.current = true 146 155 } 147 156 }, [ 148 - posts, 157 + highlightedPostIndex, 149 158 view.hasContent, 150 159 view.isFromCache, 151 160 view.isLoadingFromCache, ··· 184 193 </View> 185 194 ) 186 195 } else if (item === REPLY_PROMPT) { 187 - return <ComposePrompt onPressCompose={onPressReply} /> 196 + return ( 197 + <View 198 + style={ 199 + treeView && [pal.border, {borderBottomWidth: 1, marginBottom: 6}] 200 + }> 201 + {isDesktopWeb && <ComposePrompt onPressCompose={onPressReply} />} 202 + </View> 203 + ) 188 204 } else if (item === DELETED) { 189 205 return ( 190 206 <View style={[pal.border, pal.viewLight, styles.itemContainer]}> ··· 224 240 // due to some complexities with how flatlist works, this is the easiest way 225 241 // I could find to get a border positioned directly under the last item 226 242 // -prf 227 - return <View style={[pal.border, styles.bottomSpacer]} /> 243 + return ( 244 + <View 245 + style={[ 246 + {height: 400}, 247 + showBottomBorder && { 248 + borderTopWidth: 1, 249 + borderColor: pal.colors.border, 250 + }, 251 + treeView && {marginTop: 10}, 252 + ]} 253 + /> 254 + ) 228 255 } else if (item === CHILD_SPINNER) { 229 256 return ( 230 257 <View style={styles.childSpinner}> ··· 240 267 item={item} 241 268 onPostReply={onRefresh} 242 269 hasPrecedingItem={prev?._showChildReplyLine} 270 + treeView={treeView} 243 271 /> 244 272 ) 245 273 } 246 274 return <></> 247 275 }, 248 - [onRefresh, onPressReply, pal, posts, isTablet], 276 + [onRefresh, onPressReply, pal, posts, isTablet, treeView, showBottomBorder], 249 277 ) 250 278 251 279 // loading ··· 377 405 } 378 406 } 379 407 yield post 380 - if (isDesktopWeb && post._isHighlightedPost) { 408 + if (post._isHighlightedPost) { 381 409 yield REPLY_PROMPT 382 410 } 383 411 if (post.replies?.length) { ··· 411 439 paddingVertical: 10, 412 440 }, 413 441 childSpinner: {}, 414 - bottomSpacer: { 415 - height: 400, 416 - borderTopWidth: 1, 417 - }, 418 442 })
+103 -38
src/view/com/post-thread/PostThreadItem.tsx
··· 35 35 import {TimeElapsed} from 'view/com/util/TimeElapsed' 36 36 import {makeProfileLink} from 'lib/routes/links' 37 37 import {isDesktopWeb} from 'platform/detection' 38 + import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 38 39 39 40 export const PostThreadItem = observer(function PostThreadItem({ 40 41 item, 41 42 onPostReply, 42 43 hasPrecedingItem, 44 + treeView, 43 45 }: { 44 46 item: PostThreadItemModel 45 47 onPostReply: () => void 46 48 hasPrecedingItem: boolean 49 + treeView: boolean 47 50 }) { 48 51 const pal = usePalette('default') 49 52 const store = useStores() ··· 389 392 </> 390 393 ) 391 394 } else { 395 + const isThreadedChild = treeView && item._depth > 0 392 396 return ( 393 - <> 397 + <PostOuterWrapper 398 + item={item} 399 + hasPrecedingItem={hasPrecedingItem} 400 + treeView={treeView}> 394 401 <PostHider 395 402 testID={`postThreadItem-by-${item.post.author.handle}`} 396 403 href={itemHref} 397 - style={[ 398 - styles.outer, 399 - pal.border, 400 - pal.view, 401 - item._showParentReplyLine && hasPrecedingItem && styles.noTopBorder, 402 - styles.cursor, 403 - ]} 404 + style={[pal.view]} 404 405 moderation={item.moderation.content}> 405 406 <PostSandboxWarning /> 406 407 407 408 <View 408 - style={{flexDirection: 'row', gap: 10, paddingLeft: 8, height: 16}}> 409 + style={{ 410 + flexDirection: 'row', 411 + gap: 10, 412 + paddingLeft: 8, 413 + height: isThreadedChild ? 8 : 16, 414 + }}> 409 415 <View style={{width: 52}}> 410 - {item._showParentReplyLine && ( 416 + {!isThreadedChild && item._showParentReplyLine && ( 411 417 <View 412 418 style={[ 413 419 styles.replyLine, ··· 431 437 ]}> 432 438 <View style={styles.layoutAvi}> 433 439 <PreviewableUserAvatar 434 - size={52} 440 + size={isThreadedChild ? 24 : 52} 435 441 did={item.post.author.did} 436 442 handle={item.post.author.handle} 437 443 avatar={item.post.author.avatar} ··· 444 450 styles.replyLine, 445 451 { 446 452 flexGrow: 1, 447 - backgroundColor: pal.colors.replyLine, 453 + backgroundColor: isThreadedChild 454 + ? pal.colors.border 455 + : pal.colors.replyLine, 448 456 marginTop: 4, 449 457 }, 450 458 ]} ··· 464 472 style={styles.alert} 465 473 /> 466 474 {item.richText?.text ? ( 467 - <View style={styles.postTextContainer}> 475 + <View 476 + style={[ 477 + styles.postTextContainer, 478 + isThreadedChild && {paddingTop: 2}, 479 + ]}> 468 480 <RichText 469 481 type="post-text" 470 482 richText={item.richText} ··· 508 520 /> 509 521 </View> 510 522 </View> 523 + {item._hasMore ? ( 524 + <Link 525 + style={[ 526 + styles.loadMore, 527 + { 528 + paddingLeft: treeView ? 44 : 70, 529 + paddingTop: 0, 530 + paddingBottom: treeView ? 4 : 12, 531 + }, 532 + ]} 533 + href={itemHref} 534 + title={itemTitle} 535 + noFeedback> 536 + <Text type="sm-medium" style={pal.textLight}> 537 + More 538 + </Text> 539 + <FontAwesomeIcon 540 + icon="angle-right" 541 + color={pal.colors.textLight} 542 + size={14} 543 + /> 544 + </Link> 545 + ) : undefined} 511 546 </PostHider> 512 - {item._hasMore ? ( 513 - <Link 514 - style={[ 515 - styles.loadMore, 516 - {borderTopColor: pal.colors.border}, 517 - pal.view, 518 - ]} 519 - href={itemHref} 520 - title={itemTitle} 521 - noFeedback> 522 - <Text style={pal.link}>Continue thread...</Text> 523 - <FontAwesomeIcon 524 - icon="angle-right" 525 - style={pal.link as FontAwesomeIconStyle} 526 - size={18} 527 - /> 528 - </Link> 529 - ) : undefined} 530 - </> 547 + </PostOuterWrapper> 531 548 ) 532 549 } 533 550 }) 534 551 552 + function PostOuterWrapper({ 553 + item, 554 + hasPrecedingItem, 555 + treeView, 556 + children, 557 + }: React.PropsWithChildren<{ 558 + item: PostThreadItemModel 559 + hasPrecedingItem: boolean 560 + treeView: boolean 561 + }>) { 562 + const {isMobile} = useWebMediaQueries() 563 + const pal = usePalette('default') 564 + if (treeView && item._depth > 0) { 565 + return ( 566 + <View 567 + style={[ 568 + pal.view, 569 + styles.cursor, 570 + {flexDirection: 'row', paddingLeft: 10}, 571 + ]}> 572 + {Array.from(Array(item._depth - 1)).map((_, n: number) => ( 573 + <View 574 + key={`${item.uri}-padding-${n}`} 575 + style={{ 576 + borderLeftWidth: 2, 577 + borderLeftColor: pal.colors.border, 578 + marginLeft: 19, 579 + paddingLeft: isMobile ? 0 : 4, 580 + }} 581 + /> 582 + ))} 583 + <View style={{flex: 1}}>{children}</View> 584 + </View> 585 + ) 586 + } 587 + return ( 588 + <View 589 + style={[ 590 + styles.outer, 591 + pal.view, 592 + pal.border, 593 + item._showParentReplyLine && hasPrecedingItem && styles.noTopBorder, 594 + styles.cursor, 595 + ]}> 596 + {children} 597 + </View> 598 + ) 599 + } 600 + 535 601 function ExpandedPostDetails({ 536 602 post, 537 603 needsTranslation, ··· 600 666 flexDirection: 'row', 601 667 alignItems: 'center', 602 668 flexWrap: 'wrap', 603 - paddingBottom: 8, 669 + paddingBottom: 4, 604 670 paddingRight: 10, 605 671 }, 606 672 postTextLargeContainer: { ··· 629 695 }, 630 696 loadMore: { 631 697 flexDirection: 'row', 632 - justifyContent: 'space-between', 633 - borderTopWidth: 1, 634 - paddingLeft: 80, 635 - paddingRight: 20, 636 - paddingVertical: 12, 698 + alignItems: 'center', 699 + justifyContent: 'flex-start', 700 + gap: 4, 701 + paddingHorizontal: 20, 637 702 }, 638 703 replyLine: { 639 704 width: 2,
+2
src/view/index.ts
··· 45 45 import {faEyeSlash as farEyeSlash} from '@fortawesome/free-regular-svg-icons/faEyeSlash' 46 46 import {faFaceSmile} from '@fortawesome/free-regular-svg-icons/faFaceSmile' 47 47 import {faFire} from '@fortawesome/free-solid-svg-icons/faFire' 48 + import {faFlask} from '@fortawesome/free-solid-svg-icons' 48 49 import {faFloppyDisk} from '@fortawesome/free-regular-svg-icons/faFloppyDisk' 49 50 import {faGear} from '@fortawesome/free-solid-svg-icons/faGear' 50 51 import {faGlobe} from '@fortawesome/free-solid-svg-icons/faGlobe' ··· 144 145 farEyeSlash, 145 146 faFaceSmile, 146 147 faFire, 148 + faFlask, 147 149 faFloppyDisk, 148 150 faGear, 149 151 faGlobe,
+1
src/view/screens/PostThread.tsx
··· 74 74 uri={uri} 75 75 view={view} 76 76 onPressReply={onPressReply} 77 + treeView={store.preferences.threadTreeViewEnabled} 77 78 /> 78 79 </View> 79 80 {isMobile && (
+4 -2
src/view/screens/PreferencesHomeFeed.tsx
··· 1 1 import React, {useState} from 'react' 2 2 import {ScrollView, StyleSheet, TouchableOpacity, View} from 'react-native' 3 3 import {observer} from 'mobx-react-lite' 4 + import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 4 5 import {Slider} from '@miblanchard/react-native-slider' 5 6 import {Text} from '../com/util/text/Text' 6 7 import {useStores} from 'state/index' ··· 158 159 159 160 <View style={[pal.viewLight, styles.card]}> 160 161 <Text type="title-sm" style={[pal.text, s.pb5]}> 161 - Show Posts from My Feeds (Experimental) 162 + <FontAwesomeIcon icon="flask" color={pal.colors.text} /> Show 163 + Posts from My Feeds 162 164 </Text> 163 165 <Text style={[pal.text, s.pb10]}> 164 166 Set this setting to "Yes" to show samples of your saved feeds in 165 - your following feed. 167 + your following feed. This is an experimental feature. 166 168 </Text> 167 169 <ToggleButton 168 170 type="default-light"
+18
src/view/screens/PreferencesThreads.tsx
··· 1 1 import React from 'react' 2 2 import {ScrollView, StyleSheet, TouchableOpacity, View} from 'react-native' 3 3 import {observer} from 'mobx-react-lite' 4 + import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 4 5 import {Text} from '../com/util/text/Text' 5 6 import {useStores} from 'state/index' 6 7 import {s, colors} from 'lib/styles' ··· 76 77 label={store.preferences.threadFollowedUsersFirst ? 'Yes' : 'No'} 77 78 isSelected={store.preferences.threadFollowedUsersFirst} 78 79 onPress={store.preferences.toggleThreadFollowedUsersFirst} 80 + /> 81 + </View> 82 + 83 + <View style={[pal.viewLight, styles.card]}> 84 + <Text type="title-sm" style={[pal.text, s.pb5]}> 85 + <FontAwesomeIcon icon="flask" color={pal.colors.text} /> Threaded 86 + Mode 87 + </Text> 88 + <Text style={[pal.text, s.pb10]}> 89 + Set this setting to "Yes" to show replies in a threaded view. This 90 + is an experimental feature. 91 + </Text> 92 + <ToggleButton 93 + type="default-light" 94 + label={store.preferences.threadTreeViewEnabled ? 'Yes' : 'No'} 95 + isSelected={store.preferences.threadTreeViewEnabled} 96 + onPress={store.preferences.toggleThreadTreeViewEnabled} 79 97 /> 80 98 </View> 81 99 </View>