diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx
index 73eb9da526..d79b0ff902 100644
--- a/src/components/Menu/index.tsx
+++ b/src/components/Menu/index.tsx
@@ -190,6 +190,55 @@ export function ItemIcon({icon: Comp}: ItemIconProps) {
)
}
+export function ItemRadio({selected}: {selected: boolean}) {
+ const t = useTheme()
+ return (
+
+ {selected ? (
+
+ ) : null}
+
+ )
+}
+
+export function LabelText({children}: {children: React.ReactNode}) {
+ const t = useTheme()
+ return (
+
+ {children}
+
+ )
+}
+
export function Group({children, style}: GroupProps) {
const t = useTheme()
return (
diff --git a/src/components/Menu/index.web.tsx b/src/components/Menu/index.web.tsx
index ab0c9d20a1..bc8596218c 100644
--- a/src/components/Menu/index.web.tsx
+++ b/src/components/Menu/index.web.tsx
@@ -304,6 +304,57 @@ export function ItemIcon({icon: Comp, position = 'left'}: ItemIconProps) {
)
}
+export function ItemRadio({selected}: {selected: boolean}) {
+ const t = useTheme()
+ return (
+
+ {selected ? (
+
+ ) : null}
+
+ )
+}
+
+export function LabelText({children}: {children: React.ReactNode}) {
+ const t = useTheme()
+ return (
+
+ {children}
+
+ )
+}
+
export function Group({children}: GroupProps) {
return children
}
diff --git a/src/screens/Settings/ThreadPreferences.tsx b/src/screens/Settings/ThreadPreferences.tsx
index b1547e495e..701d3d9e56 100644
--- a/src/screens/Settings/ThreadPreferences.tsx
+++ b/src/screens/Settings/ThreadPreferences.tsx
@@ -148,7 +148,7 @@ export function ThreadPreferencesScreen({}: Props) {
}
style={[a.w_full, a.gap_md]}>
- Show replies in a threaded view
+ Show replies as threaded
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index af58edcbfe..a0073b02f9 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -1,4 +1,4 @@
-import React, {useRef} from 'react'
+import React, {memo, useRef, useState} from 'react'
import {StyleSheet, useWindowDimensions, View} from 'react-native'
import {runOnJS} from 'react-native-reanimated'
import Animated from 'react-native-reanimated'
@@ -7,6 +7,7 @@ import {AppBskyFeedDefs, AppBskyFeedThreadgate} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+import {HITSLOP_10} from '#/lib/constants'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform'
import {useSetTitle} from '#/lib/hooks/useSetTitle'
@@ -28,14 +29,18 @@ import {
ThreadPost,
usePostThreadQuery,
} from '#/state/queries/post-thread'
+import {useSetThreadViewPreferencesMutation} from '#/state/queries/preferences'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell'
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
import {List, ListMethods} from '#/view/com/util/List'
import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonIcon} from '#/components/Button'
+import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from '#/components/icons/SettingsSlider'
import {Header} from '#/components/Layout'
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
+import * as Menu from '#/components/Menu'
import {Text} from '#/components/Typography'
import {PostThreadComposePrompt} from './PostThreadComposePrompt'
import {PostThreadItem} from './PostThreadItem'
@@ -107,12 +112,47 @@ export function PostThread({uri}: {uri: string | undefined}) {
dataUpdatedAt: fetchedAt,
} = usePostThreadQuery(uri)
+ // The original source of truth for these are the server settings.
+ const serverPrefs = preferences?.threadViewPrefs
+ const serverPrioritizeFollowedUsers =
+ serverPrefs?.prioritizeFollowedUsers ?? true
+ const serverTreeViewEnabled = serverPrefs?.lab_treeViewEnabled ?? false
+ const serverSortReplies = serverPrefs?.sort ?? 'hotness'
+
+ // However, we also need these to work locally for PWI (without persistance).
+ // So we're mirroring them locally.
+ const prioritizeFollowedUsers = serverPrioritizeFollowedUsers
+ const [treeViewEnabled, setTreeViewEnabled] = useState(serverTreeViewEnabled)
+ const [sortReplies, setSortReplies] = useState(serverSortReplies)
+
+ // We'll reset the local state if new server state flows down to us.
+ const [prevServerPrefs, setPrevServerPrefs] = useState(serverPrefs)
+ if (prevServerPrefs !== serverPrefs) {
+ setPrevServerPrefs(serverPrefs)
+ setTreeViewEnabled(serverTreeViewEnabled)
+ setSortReplies(serverSortReplies)
+ }
+
+ // And we'll update the local state when mutating the server prefs.
+ const {mutate: mutateThreadViewPrefs} = useSetThreadViewPreferencesMutation()
+ function updateTreeViewEnabled(newTreeViewEnabled: boolean) {
+ setTreeViewEnabled(newTreeViewEnabled)
+ if (hasSession) {
+ mutateThreadViewPrefs({lab_treeViewEnabled: newTreeViewEnabled})
+ }
+ }
+ function updateSortReplies(newSortReplies: string) {
+ setSortReplies(newSortReplies)
+ if (hasSession) {
+ mutateThreadViewPrefs({sort: newSortReplies})
+ }
+ }
+
const treeView = React.useMemo(
- () =>
- !!preferences?.threadViewPrefs?.lab_treeViewEnabled &&
- hasBranchingReplies(thread),
- [preferences?.threadViewPrefs, thread],
+ () => treeViewEnabled && hasBranchingReplies(thread),
+ [treeViewEnabled, thread],
)
+
const rootPost = thread?.type === 'post' ? thread.post : undefined
const rootPostRecord = thread?.type === 'post' ? thread.record : undefined
const threadgateRecord = threadgate?.record as
@@ -175,13 +215,16 @@ export function PostThread({uri}: {uri: string | undefined}) {
const [fetchedAtCache] = React.useState(() => new Map())
const [randomCache] = React.useState(() => new Map())
const skeleton = React.useMemo(() => {
- const threadViewPrefs = preferences?.threadViewPrefs
- if (!threadViewPrefs || !thread) return null
-
+ if (!thread) return null
return createThreadSkeleton(
sortThread(
thread,
- threadViewPrefs,
+ {
+ // Prefer local state as the source of truth.
+ sort: sortReplies,
+ lab_treeViewEnabled: treeViewEnabled,
+ prioritizeFollowedUsers,
+ },
threadModerationCache,
currentDid,
justPostedUris,
@@ -198,7 +241,9 @@ export function PostThread({uri}: {uri: string | undefined}) {
)
}, [
thread,
- preferences?.threadViewPrefs,
+ prioritizeFollowedUsers,
+ sortReplies,
+ treeViewEnabled,
currentDid,
treeView,
threadModerationCache,
@@ -484,14 +529,21 @@ export function PostThread({uri}: {uri: string | undefined}) {
return (
<>
-
+
Post
-
+
+
+
@@ -537,6 +589,122 @@ export function PostThread({uri}: {uri: string | undefined}) {
)
}
+let ThreadMenu = ({
+ sortReplies,
+ treeViewEnabled,
+ setSortReplies,
+ setTreeViewEnabled,
+}: {
+ sortReplies: string
+ treeViewEnabled: boolean
+ setSortReplies: (newValue: string) => void
+ setTreeViewEnabled: (newValue: boolean) => void
+}): React.ReactNode => {
+ const {_} = useLingui()
+ return (
+
+
+ {({props}) => (
+
+ )}
+
+
+
+ Show replies as
+
+
+ {
+ setTreeViewEnabled(false)
+ }}>
+
+ Linear
+
+
+
+ {
+ setTreeViewEnabled(true)
+ }}>
+
+ Threaded
+
+
+
+
+
+
+ Reply sorting
+
+
+ {
+ setSortReplies('hotness')
+ }}>
+
+ Hot replies first
+
+
+
+ {
+ setSortReplies('oldest')
+ }}>
+
+ Oldest replies first
+
+
+
+ {
+ setSortReplies('newest')
+ }}>
+
+ Newest replies first
+
+
+
+ {
+ setSortReplies('most-likes')
+ }}>
+
+ Most-liked replies first
+
+
+
+ {
+ setSortReplies('random')
+ }}>
+
+ Random (aka "Poster's Roulette")
+
+
+
+
+
+
+ )
+}
+ThreadMenu = memo(ThreadMenu)
+
function MobileComposePrompt({onPressReply}: {onPressReply: () => unknown}) {
const safeAreaInsets = useSafeAreaInsets()
const fabMinimalShellTransform = useMinimalShellFabTransform()