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
zio/stable
Paul Frazee 2023-09-19 19:08:11 -07:00 committed by GitHub
parent d2c253a284
commit 1af8e83d53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 178 additions and 53 deletions

View File

@ -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
}

View File

@ -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({
</View>
)
} else if (item === REPLY_PROMPT) {
return <ComposePrompt onPressCompose={onPressReply} />
return (
<View
style={
treeView && [pal.border, {borderBottomWidth: 1, marginBottom: 6}]
}>
{isDesktopWeb && <ComposePrompt onPressCompose={onPressReply} />}
</View>
)
} else if (item === DELETED) {
return (
<View style={[pal.border, pal.viewLight, styles.itemContainer]}>
@ -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 <View style={[pal.border, styles.bottomSpacer]} />
return (
<View
style={[
{height: 400},
showBottomBorder && {
borderTopWidth: 1,
borderColor: pal.colors.border,
},
treeView && {marginTop: 10},
]}
/>
)
} else if (item === CHILD_SPINNER) {
return (
<View style={styles.childSpinner}>
@ -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,
},
})

View File

@ -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 (
<>
<PostOuterWrapper
item={item}
hasPrecedingItem={hasPrecedingItem}
treeView={treeView}>
<PostHider
testID={`postThreadItem-by-${item.post.author.handle}`}
href={itemHref}
style={[
styles.outer,
pal.border,
pal.view,
item._showParentReplyLine && hasPrecedingItem && styles.noTopBorder,
styles.cursor,
]}
style={[pal.view]}
moderation={item.moderation.content}>
<PostSandboxWarning />
<View
style={{flexDirection: 'row', gap: 10, paddingLeft: 8, height: 16}}>
style={{
flexDirection: 'row',
gap: 10,
paddingLeft: 8,
height: isThreadedChild ? 8 : 16,
}}>
<View style={{width: 52}}>
{item._showParentReplyLine && (
{!isThreadedChild && item._showParentReplyLine && (
<View
style={[
styles.replyLine,
@ -431,7 +437,7 @@ export const PostThreadItem = observer(function PostThreadItem({
]}>
<View style={styles.layoutAvi}>
<PreviewableUserAvatar
size={52}
size={isThreadedChild ? 24 : 52}
did={item.post.author.did}
handle={item.post.author.handle}
avatar={item.post.author.avatar}
@ -444,7 +450,9 @@ export const PostThreadItem = observer(function PostThreadItem({
styles.replyLine,
{
flexGrow: 1,
backgroundColor: pal.colors.replyLine,
backgroundColor: isThreadedChild
? pal.colors.border
: pal.colors.replyLine,
marginTop: 4,
},
]}
@ -464,7 +472,11 @@ export const PostThreadItem = observer(function PostThreadItem({
style={styles.alert}
/>
{item.richText?.text ? (
<View style={styles.postTextContainer}>
<View
style={[
styles.postTextContainer,
isThreadedChild && {paddingTop: 2},
]}>
<RichText
type="post-text"
richText={item.richText}
@ -508,30 +520,84 @@ export const PostThreadItem = observer(function PostThreadItem({
/>
</View>
</View>
{item._hasMore ? (
<Link
style={[
styles.loadMore,
{
paddingLeft: treeView ? 44 : 70,
paddingTop: 0,
paddingBottom: treeView ? 4 : 12,
},
]}
href={itemHref}
title={itemTitle}
noFeedback>
<Text type="sm-medium" style={pal.textLight}>
More
</Text>
<FontAwesomeIcon
icon="angle-right"
color={pal.colors.textLight}
size={14}
/>
</Link>
) : undefined}
</PostHider>
{item._hasMore ? (
<Link
style={[
styles.loadMore,
{borderTopColor: pal.colors.border},
pal.view,
]}
href={itemHref}
title={itemTitle}
noFeedback>
<Text style={pal.link}>Continue thread...</Text>
<FontAwesomeIcon
icon="angle-right"
style={pal.link as FontAwesomeIconStyle}
size={18}
/>
</Link>
) : undefined}
</>
</PostOuterWrapper>
)
}
})
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 (
<View
style={[
pal.view,
styles.cursor,
{flexDirection: 'row', paddingLeft: 10},
]}>
{Array.from(Array(item._depth - 1)).map((_, n: number) => (
<View
key={`${item.uri}-padding-${n}`}
style={{
borderLeftWidth: 2,
borderLeftColor: pal.colors.border,
marginLeft: 19,
paddingLeft: isMobile ? 0 : 4,
}}
/>
))}
<View style={{flex: 1}}>{children}</View>
</View>
)
}
return (
<View
style={[
styles.outer,
pal.view,
pal.border,
item._showParentReplyLine && hasPrecedingItem && styles.noTopBorder,
styles.cursor,
]}>
{children}
</View>
)
}
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,

View File

@ -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,

View File

@ -74,6 +74,7 @@ export const PostThreadScreen = withAuthRequired(({route}: Props) => {
uri={uri}
view={view}
onPressReply={onPressReply}
treeView={store.preferences.threadTreeViewEnabled}
/>
</View>
{isMobile && (

View File

@ -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({
<View style={[pal.viewLight, styles.card]}>
<Text type="title-sm" style={[pal.text, s.pb5]}>
Show Posts from My Feeds (Experimental)
<FontAwesomeIcon icon="flask" color={pal.colors.text} /> Show
Posts from My Feeds
</Text>
<Text style={[pal.text, s.pb10]}>
Set this setting to "Yes" to show samples of your saved feeds in
your following feed.
your following feed. This is an experimental feature.
</Text>
<ToggleButton
type="default-light"

View File

@ -1,6 +1,7 @@
import React from 'react'
import {ScrollView, StyleSheet, TouchableOpacity, View} from 'react-native'
import {observer} from 'mobx-react-lite'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Text} from '../com/util/text/Text'
import {useStores} from 'state/index'
import {s, colors} from 'lib/styles'
@ -78,6 +79,23 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
onPress={store.preferences.toggleThreadFollowedUsersFirst}
/>
</View>
<View style={[pal.viewLight, styles.card]}>
<Text type="title-sm" style={[pal.text, s.pb5]}>
<FontAwesomeIcon icon="flask" color={pal.colors.text} /> Threaded
Mode
</Text>
<Text style={[pal.text, s.pb10]}>
Set this setting to "Yes" to show replies in a threaded view. This
is an experimental feature.
</Text>
<ToggleButton
type="default-light"
label={store.preferences.threadTreeViewEnabled ? 'Yes' : 'No'}
isSelected={store.preferences.threadTreeViewEnabled}
onPress={store.preferences.toggleThreadTreeViewEnabled}
/>
</View>
</View>
</ScrollView>