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.
+
+
+