From 1af8e83d536cf6a9db128409c8e00a0b44d9a985 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 19 Sep 2023 19:08:11 -0700 Subject: [PATCH] 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 --- src/state/models/ui/preferences.ts | 15 ++- src/view/com/post-thread/PostThread.tsx | 48 +++++-- src/view/com/post-thread/PostThreadItem.tsx | 141 ++++++++++++++------ src/view/index.ts | 2 + src/view/screens/PostThread.tsx | 1 + src/view/screens/PreferencesHomeFeed.tsx | 6 +- src/view/screens/PreferencesThreads.tsx | 18 +++ 7 files changed, 178 insertions(+), 53 deletions(-) diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts index 03f08bc1bb..5c6ea230be 100644 --- a/src/state/models/ui/preferences.ts +++ b/src/state/models/ui/preferences.ts @@ -58,6 +58,7 @@ export class PreferencesModel { homeFeedMergeFeedEnabled: boolean = false threadDefaultSort: string = 'oldest' threadFollowedUsersFirst: boolean = true + threadTreeViewEnabled: boolean = false requireAltTextEnabled: boolean = false // used to linearize async modifications to state @@ -91,6 +92,7 @@ export class PreferencesModel { homeFeedMergeFeedEnabled: this.homeFeedMergeFeedEnabled, threadDefaultSort: this.threadDefaultSort, threadFollowedUsersFirst: this.threadFollowedUsersFirst, + threadTreeViewEnabled: this.threadTreeViewEnabled, requireAltTextEnabled: this.requireAltTextEnabled, } } @@ -202,13 +204,20 @@ export class PreferencesModel { ) { this.threadDefaultSort = v.threadDefaultSort } - // check if tread followed-users-first is enabled in preferences, then hydrate + // check if thread followed-users-first is enabled in preferences, then hydrate if ( hasProp(v, 'threadFollowedUsersFirst') && typeof v.threadFollowedUsersFirst === 'boolean' ) { this.threadFollowedUsersFirst = v.threadFollowedUsersFirst } + // check if thread treeview is enabled in preferences, then hydrate + if ( + hasProp(v, 'threadTreeViewEnabled') && + typeof v.threadTreeViewEnabled === 'boolean' + ) { + this.threadTreeViewEnabled = v.threadTreeViewEnabled + } // check if requiring alt text is enabled in preferences, then hydrate if ( hasProp(v, 'requireAltTextEnabled') && @@ -524,6 +533,10 @@ export class PreferencesModel { this.threadFollowedUsersFirst = !this.threadFollowedUsersFirst } + toggleThreadTreeViewEnabled() { + this.threadTreeViewEnabled = !this.threadTreeViewEnabled + } + toggleRequireAltTextEnabled() { this.requireAltTextEnabled = !this.requireAltTextEnabled } diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index 1cc177d17b..373b4499da 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -55,6 +55,7 @@ const LOAD_MORE = { const BOTTOM_COMPONENT = { _reactKey: '__bottom_component__', _isHighlightedPost: false, + _showBorder: true, } type YieldedItem = | PostThreadItemModel @@ -69,10 +70,12 @@ export const PostThread = observer(function PostThread({ uri, view, onPressReply, + treeView, }: { uri: string view: PostThreadModel onPressReply: () => void + treeView: boolean }) { const pal = usePalette('default') const {isTablet} = useWebMediaQueries() @@ -99,6 +102,13 @@ export const PostThread = observer(function PostThread({ } return [] }, [view.isLoadingFromCache, view.thread, maxVisible]) + const highlightedPostIndex = posts.findIndex(post => post._isHighlightedPost) + const showBottomBorder = + !treeView || + // in the treeview, only show the bottom border + // if there are replies under the highlighted posts + posts.findLast(v => v instanceof PostThreadItemModel) !== + posts[highlightedPostIndex] useSetTitle( view.thread?.postRecord && `${sanitizeDisplayName( @@ -135,17 +145,16 @@ export const PostThread = observer(function PostThread({ return } - const index = posts.findIndex(post => post._isHighlightedPost) - if (index !== -1) { + if (highlightedPostIndex !== -1) { ref.current?.scrollToIndex({ - index, + index: highlightedPostIndex, animated: false, viewPosition: 0, }) hasScrolledIntoView.current = true } }, [ - posts, + highlightedPostIndex, view.hasContent, view.isFromCache, view.isLoadingFromCache, @@ -184,7 +193,14 @@ export const PostThread = observer(function PostThread({ ) } else if (item === REPLY_PROMPT) { - return + return ( + + {isDesktopWeb && } + + ) } else if (item === DELETED) { return ( @@ -224,7 +240,18 @@ export const PostThread = observer(function PostThread({ // due to some complexities with how flatlist works, this is the easiest way // I could find to get a border positioned directly under the last item // -prf - return + return ( + + ) } else if (item === CHILD_SPINNER) { return ( @@ -240,12 +267,13 @@ export const PostThread = observer(function PostThread({ item={item} onPostReply={onRefresh} hasPrecedingItem={prev?._showChildReplyLine} + treeView={treeView} /> ) } return <> }, - [onRefresh, onPressReply, pal, posts, isTablet], + [onRefresh, onPressReply, pal, posts, isTablet, treeView, showBottomBorder], ) // loading @@ -377,7 +405,7 @@ function* flattenThread( } } yield post - if (isDesktopWeb && post._isHighlightedPost) { + if (post._isHighlightedPost) { yield REPLY_PROMPT } if (post.replies?.length) { @@ -411,8 +439,4 @@ const styles = StyleSheet.create({ paddingVertical: 10, }, childSpinner: {}, - bottomSpacer: { - height: 400, - borderTopWidth: 1, - }, }) diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 37c7ece471..1089bfabf8 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -35,15 +35,18 @@ import {formatCount} from '../util/numeric/format' import {TimeElapsed} from 'view/com/util/TimeElapsed' import {makeProfileLink} from 'lib/routes/links' import {isDesktopWeb} from 'platform/detection' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' export const PostThreadItem = observer(function PostThreadItem({ item, onPostReply, hasPrecedingItem, + treeView, }: { item: PostThreadItemModel onPostReply: () => void hasPrecedingItem: boolean + treeView: boolean }) { const pal = usePalette('default') const store = useStores() @@ -389,25 +392,28 @@ export const PostThreadItem = observer(function PostThreadItem({ ) } else { + const isThreadedChild = treeView && item._depth > 0 return ( - <> + + style={{ + flexDirection: 'row', + gap: 10, + paddingLeft: 8, + height: isThreadedChild ? 8 : 16, + }}> - {item._showParentReplyLine && ( + {!isThreadedChild && item._showParentReplyLine && ( {item.richText?.text ? ( - + + {item._hasMore ? ( + + + More + + + + ) : undefined} - {item._hasMore ? ( - - Continue thread... - - - ) : undefined} - + ) } }) +function PostOuterWrapper({ + item, + hasPrecedingItem, + treeView, + children, +}: React.PropsWithChildren<{ + item: PostThreadItemModel + hasPrecedingItem: boolean + treeView: boolean +}>) { + const {isMobile} = useWebMediaQueries() + const pal = usePalette('default') + if (treeView && item._depth > 0) { + return ( + + {Array.from(Array(item._depth - 1)).map((_, n: number) => ( + + ))} + {children} + + ) + } + return ( + + {children} + + ) +} + function ExpandedPostDetails({ post, needsTranslation, @@ -600,7 +666,7 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap', - paddingBottom: 8, + paddingBottom: 4, paddingRight: 10, }, postTextLargeContainer: { @@ -629,11 +695,10 @@ const styles = StyleSheet.create({ }, loadMore: { flexDirection: 'row', - justifyContent: 'space-between', - borderTopWidth: 1, - paddingLeft: 80, - paddingRight: 20, - paddingVertical: 12, + alignItems: 'center', + justifyContent: 'flex-start', + gap: 4, + paddingHorizontal: 20, }, replyLine: { width: 2, diff --git a/src/view/index.ts b/src/view/index.ts index da1b781463..07848aa8fa 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -45,6 +45,7 @@ import {faEye} from '@fortawesome/free-solid-svg-icons/faEye' import {faEyeSlash as farEyeSlash} from '@fortawesome/free-regular-svg-icons/faEyeSlash' import {faFaceSmile} from '@fortawesome/free-regular-svg-icons/faFaceSmile' import {faFire} from '@fortawesome/free-solid-svg-icons/faFire' +import {faFlask} from '@fortawesome/free-solid-svg-icons' import {faFloppyDisk} from '@fortawesome/free-regular-svg-icons/faFloppyDisk' import {faGear} from '@fortawesome/free-solid-svg-icons/faGear' import {faGlobe} from '@fortawesome/free-solid-svg-icons/faGlobe' @@ -144,6 +145,7 @@ export function setup() { farEyeSlash, faFaceSmile, faFire, + faFlask, faFloppyDisk, faGear, faGlobe, diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx index a6aafa5302..90b98d0527 100644 --- a/src/view/screens/PostThread.tsx +++ b/src/view/screens/PostThread.tsx @@ -74,6 +74,7 @@ export const PostThreadScreen = withAuthRequired(({route}: Props) => { uri={uri} view={view} onPressReply={onPressReply} + treeView={store.preferences.threadTreeViewEnabled} /> {isMobile && ( diff --git a/src/view/screens/PreferencesHomeFeed.tsx b/src/view/screens/PreferencesHomeFeed.tsx index 34139bec14..404d006f8d 100644 --- a/src/view/screens/PreferencesHomeFeed.tsx +++ b/src/view/screens/PreferencesHomeFeed.tsx @@ -1,6 +1,7 @@ import React, {useState} from 'react' import {ScrollView, StyleSheet, TouchableOpacity, View} from 'react-native' import {observer} from 'mobx-react-lite' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Slider} from '@miblanchard/react-native-slider' import {Text} from '../com/util/text/Text' import {useStores} from 'state/index' @@ -158,11 +159,12 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({ - Show Posts from My Feeds (Experimental) + Show + Posts from My Feeds Set this setting to "Yes" to show samples of your saved feeds in - your following feed. + your following feed. This is an experimental feature. + + + + Threaded + Mode + + + Set this setting to "Yes" to show replies in a threaded view. This + is an experimental feature. + + +