diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts
index ef6dc1d4df..90ccd3443f 100644
--- a/src/lib/statsig/gates.ts
+++ b/src/lib/statsig/gates.ts
@@ -8,7 +8,6 @@ export type Gate =
| 'old_postonboarding'
| 'onboarding_add_video_feed'
| 'post_follow_profile_suggested_accounts'
- | 'post_threads_v2_unspecced'
| 'remove_show_latest_button'
| 'test_gate_1'
| 'test_gate_2'
diff --git a/src/view/com/post-thread/PostThreadComposePrompt.tsx b/src/screens/PostThread/components/ThreadComposePrompt.tsx
similarity index 98%
rename from src/view/com/post-thread/PostThreadComposePrompt.tsx
rename to src/screens/PostThread/components/ThreadComposePrompt.tsx
index dc05617258..e12c7e766f 100644
--- a/src/view/com/post-thread/PostThreadComposePrompt.tsx
+++ b/src/screens/PostThread/components/ThreadComposePrompt.tsx
@@ -14,7 +14,7 @@ import {transparentifyColor} from '#/alf/util/colorGeneration'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {Text} from '#/components/Typography'
-export function PostThreadComposePrompt({
+export function ThreadComposePrompt({
onPressCompose,
style,
}: {
diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx
index fc1f1caeb6..550bddc6a6 100644
--- a/src/screens/PostThread/components/ThreadItemAnchor.tsx
+++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx
@@ -32,9 +32,9 @@ import {useSession} from '#/state/session'
import {type OnPostSuccessData} from '#/state/shell/composer'
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
import {type PostSource} from '#/state/unstable-post-source'
-import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn'
import {formatCount} from '#/view/com/util/numeric/format'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
+import {ThreadItemAnchorFollowButton} from '#/screens/PostThread/components/ThreadItemAnchorFollowButton'
import {
LINEAR_AVI_WIDTH,
OUTER_SPACE,
@@ -367,7 +367,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
{showFollowButton && (
-
+
)}
diff --git a/src/view/com/post-thread/PostThreadFollowBtn.tsx b/src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx
similarity index 98%
rename from src/view/com/post-thread/PostThreadFollowBtn.tsx
rename to src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx
index fc9296cad5..d4cf120cf8 100644
--- a/src/view/com/post-thread/PostThreadFollowBtn.tsx
+++ b/src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx
@@ -17,7 +17,7 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
-export function PostThreadFollowBtn({did}: {did: string}) {
+export function ThreadItemAnchorFollowButton({did}: {did: string}) {
const {data: profile, isLoading} = useProfileQuery({did})
// We will never hit this - the profile will always be cached or loaded above
diff --git a/src/screens/PostThread/index.tsx b/src/screens/PostThread/index.tsx
index f91daf54bc..7432f71db8 100644
--- a/src/screens/PostThread/index.tsx
+++ b/src/screens/PostThread/index.tsx
@@ -12,9 +12,9 @@ import {useSession} from '#/state/session'
import {type OnPostSuccessData} from '#/state/shell/composer'
import {useShellLayout} from '#/state/shell/shell-layout'
import {useUnstablePostSource} from '#/state/unstable-post-source'
-import {PostThreadComposePrompt} from '#/view/com/post-thread/PostThreadComposePrompt'
import {List, type ListMethods} from '#/view/com/util/List'
import {HeaderDropdown} from '#/screens/PostThread/components/HeaderDropdown'
+import {ThreadComposePrompt} from '#/screens/PostThread/components/ThreadComposePrompt'
import {ThreadError} from '#/screens/PostThread/components/ThreadError'
import {
ThreadItemAnchor,
@@ -455,7 +455,7 @@ export function PostThread({uri}: {uri: string}) {
return (
{gtMobile && (
-
+
)}
)
@@ -586,7 +586,7 @@ function MobileComposePrompt({onPressReply}: {onPressReply: () => unknown}) {
return (
-
+
)
}
diff --git a/src/screens/Settings/ThreadPreferences.tsx b/src/screens/Settings/ThreadPreferences.tsx
index af3cf915f5..cba896a764 100644
--- a/src/screens/Settings/ThreadPreferences.tsx
+++ b/src/screens/Settings/ThreadPreferences.tsx
@@ -6,11 +6,6 @@ import {
type CommonNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
-import {useGate} from '#/lib/statsig/statsig'
-import {
- usePreferencesQuery,
- useSetThreadViewPreferencesMutation,
-} from '#/state/queries/preferences'
import {
normalizeSort,
normalizeView,
@@ -18,7 +13,6 @@ import {
} from '#/state/queries/preferences/useThreadPreferences'
import {atoms as a, useTheme} from '#/alf'
import * as Toggle from '#/components/forms/Toggle'
-import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker'
import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble'
import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person'
import {Tree_Stroke2_Corner0_Rounded as TreeIcon} from '#/components/icons/Tree'
@@ -28,16 +22,6 @@ import * as SettingsList from './components/SettingsList'
type Props = NativeStackScreenProps
export function ThreadPreferencesScreen({}: Props) {
- const gate = useGate()
-
- return gate('post_threads_v2_unspecced') ? (
-
- ) : (
-
- )
-}
-
-export function ThreadPreferencesV2() {
const t = useTheme()
const {_} = useLingui()
const {
@@ -150,145 +134,3 @@ export function ThreadPreferencesV2() {
)
}
-
-export function ThreadPreferencesV1() {
- const {_} = useLingui()
- const t = useTheme()
-
- const {data: preferences} = usePreferencesQuery()
- const {mutate: setThreadViewPrefs, variables} =
- useSetThreadViewPreferencesMutation()
-
- const sortReplies = variables?.sort ?? preferences?.threadViewPrefs?.sort
-
- const prioritizeFollowedUsers = Boolean(
- variables?.prioritizeFollowedUsers ??
- preferences?.threadViewPrefs?.prioritizeFollowedUsers,
- )
- const treeViewEnabled = Boolean(
- variables?.lab_treeViewEnabled ??
- preferences?.threadViewPrefs?.lab_treeViewEnabled,
- )
-
- return (
-
-
-
-
-
- Thread Preferences
-
-
-
-
-
-
-
-
-
- Sort replies
-
-
-
- Sort replies to the same post by:
-
- setThreadViewPrefs({sort: values[0]})}>
-
-
-
-
- Hot replies first
-
-
-
-
-
- Oldest replies first
-
-
-
-
-
- Newest replies first
-
-
-
-
-
- Most-liked first
-
-
-
-
-
- Random (aka "Poster's Roulette")
-
-
-
-
-
-
-
-
-
- Prioritize your Follows
-
-
- setThreadViewPrefs({
- prioritizeFollowedUsers: value,
- })
- }
- style={[a.w_full, a.gap_md]}>
-
-
- Show replies by people you follow before all other replies
-
-
-
-
-
-
-
-
-
- Experimental
-
-
- setThreadViewPrefs({
- lab_treeViewEnabled: value,
- })
- }
- style={[a.w_full, a.gap_md]}>
-
- Show replies as threaded
-
-
-
-
-
-
-
- )
-}
diff --git a/src/screens/VideoFeed/index.tsx b/src/screens/VideoFeed/index.tsx
index b535930104..22989e6c20 100644
--- a/src/screens/VideoFeed/index.tsx
+++ b/src/screens/VideoFeed/index.tsx
@@ -80,9 +80,9 @@ import {useProfileFollowMutationQueue} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell'
import {useSetLightStatusBar} from '#/state/shell/light-status-bar'
-import {PostThreadComposePrompt} from '#/view/com/post-thread/PostThreadComposePrompt'
import {List} from '#/view/com/util/List'
import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {ThreadComposePrompt} from '#/screens/PostThread/components/ThreadComposePrompt'
import {Header} from '#/screens/VideoFeed/components/Header'
import {atoms as a, ios, platform, ThemeProvider, useTheme} from '#/alf'
import {setSystemUITheme} from '#/alf/util/systemUI'
@@ -883,7 +883,7 @@ function Overlay({
player={player}
seekingAnimationSV={seekingAnimationSV}
scrollGesture={scrollGesture}>
-
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
deleted file mode 100644
index bbf9f4a200..0000000000
--- a/src/view/com/post-thread/PostThread.tsx
+++ /dev/null
@@ -1,910 +0,0 @@
-import React, {memo, useRef, useState} from 'react'
-import {useWindowDimensions, View} from 'react-native'
-import {runOnJS, useAnimatedStyle} from 'react-native-reanimated'
-import Animated from 'react-native-reanimated'
-import {
- AppBskyFeedDefs,
- type AppBskyFeedThreadgate,
- moderatePost,
-} 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 {useOpenComposer} from '#/lib/hooks/useOpenComposer'
-import {useSetTitle} from '#/lib/hooks/useSetTitle'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {ScrollProvider} from '#/lib/ScrollContext'
-import {sanitizeDisplayName} from '#/lib/strings/display-names'
-import {cleanError} from '#/lib/strings/errors'
-import {isAndroid, isNative, isWeb} from '#/platform/detection'
-import {useFeedFeedback} from '#/state/feed-feedback'
-import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {
- fillThreadModerationCache,
- sortThread,
- type ThreadBlocked,
- type ThreadModerationCache,
- type ThreadNode,
- type ThreadNotFound,
- type 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 {useShellLayout} from '#/state/shell/shell-layout'
-import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
-import {useUnstablePostSource} from '#/state/unstable-post-source'
-import {List, type 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'
-import {PostThreadLoadMore} from './PostThreadLoadMore'
-import {PostThreadShowHiddenReplies} from './PostThreadShowHiddenReplies'
-
-// FlatList maintainVisibleContentPosition breaks if too many items
-// are prepended. This seems to be an optimal number based on *shrug*.
-const PARENTS_CHUNK_SIZE = 15
-
-const MAINTAIN_VISIBLE_CONTENT_POSITION = {
- // We don't insert any elements before the root row while loading.
- // So the row we want to use as the scroll anchor is the first row.
- minIndexForVisible: 0,
-}
-
-const REPLY_PROMPT = {_reactKey: '__reply__'}
-const LOAD_MORE = {_reactKey: '__load_more__'}
-const SHOW_HIDDEN_REPLIES = {_reactKey: '__show_hidden_replies__'}
-const SHOW_MUTED_REPLIES = {_reactKey: '__show_muted_replies__'}
-
-enum HiddenRepliesState {
- Hide,
- Show,
- ShowAndOverridePostHider,
-}
-
-type YieldedItem =
- | ThreadPost
- | ThreadBlocked
- | ThreadNotFound
- | typeof SHOW_HIDDEN_REPLIES
- | typeof SHOW_MUTED_REPLIES
-type RowItem =
- | YieldedItem
- // TODO: TS doesn't actually enforce it's one of these, it only enforces matching shape.
- | typeof REPLY_PROMPT
- | typeof LOAD_MORE
-
-type ThreadSkeletonParts = {
- parents: YieldedItem[]
- highlightedPost: ThreadNode
- replies: YieldedItem[]
-}
-
-const keyExtractor = (item: RowItem) => {
- return item._reactKey
-}
-
-export function PostThread({uri}: {uri: string}) {
- const {hasSession, currentAccount} = useSession()
- const {_} = useLingui()
- const t = useTheme()
- const {isMobile} = useWebMediaQueries()
- const initialNumToRender = useInitialNumToRender()
- const {height: windowHeight} = useWindowDimensions()
- const [hiddenRepliesState, setHiddenRepliesState] = React.useState(
- HiddenRepliesState.Hide,
- )
- const headerRef = React.useRef(null)
- const anchorPostSource = useUnstablePostSource(uri)
- const feedFeedback = useFeedFeedback(anchorPostSource?.feed, hasSession)
-
- const {data: preferences} = usePreferencesQuery()
- const {
- isFetching,
- isError: isThreadError,
- error: threadError,
- refetch,
- data: {thread, threadgate} = {},
- 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 persistence).
- // 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(
- () => 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
- | AppBskyFeedThreadgate.Record
- | undefined
- const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({
- threadgateRecord,
- })
-
- const moderationOpts = useModerationOpts()
- const isNoPwi = React.useMemo(() => {
- const mod =
- rootPost && moderationOpts
- ? moderatePost(rootPost, moderationOpts)
- : undefined
- return !!mod
- ?.ui('contentList')
- .blurs.find(
- cause =>
- cause.type === 'label' &&
- cause.labelDef.identifier === '!no-unauthenticated',
- )
- }, [rootPost, moderationOpts])
-
- // Values used for proper rendering of parents
- const ref = useRef(null)
- const highlightedPostRef = useRef(null)
- const [maxParents, setMaxParents] = React.useState(
- isWeb ? Infinity : PARENTS_CHUNK_SIZE,
- )
- const [maxReplies, setMaxReplies] = React.useState(50)
-
- useSetTitle(
- rootPost && !isNoPwi
- ? `${sanitizeDisplayName(
- rootPost.author.displayName || `@${rootPost.author.handle}`,
- )}: "${rootPostRecord!.text}"`
- : '',
- )
-
- // On native, this is going to start out `true`. We'll toggle it to `false` after the initial render if flushed.
- // This ensures that the first render contains no parents--even if they are already available in the cache.
- // We need to delay showing them so that we can use maintainVisibleContentPosition to keep the main post on screen.
- // On the web this is not necessary because we can synchronously adjust the scroll in onContentSizeChange instead.
- const [deferParents, setDeferParents] = React.useState(isNative)
-
- const currentDid = currentAccount?.did
- const threadModerationCache = React.useMemo(() => {
- const cache: ThreadModerationCache = new WeakMap()
- if (thread && moderationOpts) {
- fillThreadModerationCache(cache, thread, moderationOpts)
- }
- return cache
- }, [thread, moderationOpts])
-
- const [justPostedUris, setJustPostedUris] = React.useState(
- () => new Set(),
- )
-
- const [fetchedAtCache] = React.useState(() => new Map())
- const [randomCache] = React.useState(() => new Map())
- const skeleton = React.useMemo(() => {
- if (!thread) return null
- return createThreadSkeleton(
- sortThread(
- thread,
- {
- // Prefer local state as the source of truth.
- sort: sortReplies,
- lab_treeViewEnabled: treeViewEnabled,
- prioritizeFollowedUsers,
- },
- threadModerationCache,
- currentDid,
- justPostedUris,
- threadgateHiddenReplies,
- fetchedAtCache,
- fetchedAt,
- randomCache,
- ),
- currentDid,
- treeView,
- threadModerationCache,
- hiddenRepliesState !== HiddenRepliesState.Hide,
- threadgateHiddenReplies,
- )
- }, [
- thread,
- prioritizeFollowedUsers,
- sortReplies,
- treeViewEnabled,
- currentDid,
- treeView,
- threadModerationCache,
- hiddenRepliesState,
- justPostedUris,
- threadgateHiddenReplies,
- fetchedAtCache,
- fetchedAt,
- randomCache,
- ])
-
- const error = React.useMemo(() => {
- if (AppBskyFeedDefs.isNotFoundPost(thread)) {
- return {
- title: _(msg`Post not found`),
- message: _(msg`The post may have been deleted.`),
- }
- } else if (skeleton?.highlightedPost.type === 'blocked') {
- return {
- title: _(msg`Post hidden`),
- message: _(
- msg`You have blocked the author or you have been blocked by the author.`,
- ),
- }
- } else if (threadError?.message.startsWith('Post not found')) {
- return {
- title: _(msg`Post not found`),
- message: _(msg`The post may have been deleted.`),
- }
- } else if (isThreadError) {
- return {
- message: threadError ? cleanError(threadError) : undefined,
- }
- }
-
- return null
- }, [thread, skeleton?.highlightedPost, isThreadError, _, threadError])
-
- // construct content
- const posts = React.useMemo(() => {
- if (!skeleton) return []
-
- const {parents, highlightedPost, replies} = skeleton
- let arr: RowItem[] = []
- if (highlightedPost.type === 'post') {
- // We want to wait for parents to load before rendering.
- // If you add something here, you'll need to update both
- // maintainVisibleContentPosition and onContentSizeChange
- // to "hold onto" the correct row instead of the first one.
-
- /*
- * This is basically `!!parents.length`, see notes on `isParentLoading`
- */
- if (!highlightedPost.ctx.isParentLoading && !deferParents) {
- // When progressively revealing parents, rendering a placeholder
- // here will cause scrolling jumps. Don't add it unless you test it.
- // QT'ing this thread is a great way to test all the scrolling hacks:
- // https://bsky.app/profile/samuel.bsky.team/post/3kjqhblh6qk2o
-
- // Everything is loaded
- let startIndex = Math.max(0, parents.length - maxParents)
- for (let i = startIndex; i < parents.length; i++) {
- arr.push(parents[i])
- }
- }
- arr.push(highlightedPost)
- if (!highlightedPost.post.viewer?.replyDisabled) {
- arr.push(REPLY_PROMPT)
- }
- for (let i = 0; i < replies.length; i++) {
- arr.push(replies[i])
- if (i === maxReplies) {
- break
- }
- }
- }
- return arr
- }, [skeleton, deferParents, maxParents, maxReplies])
-
- // This is only used on the web to keep the post in view when its parents load.
- // On native, we rely on `maintainVisibleContentPosition` instead.
- const didAdjustScrollWeb = useRef(false)
- const onContentSizeChangeWeb = React.useCallback(() => {
- // only run once
- if (didAdjustScrollWeb.current) {
- return
- }
- // wait for loading to finish
- if (thread?.type === 'post' && !!thread.parent) {
- // Measure synchronously to avoid a layout jump.
- const postNode = highlightedPostRef.current
- const headerNode = headerRef.current
- if (postNode && headerNode) {
- let pageY = (postNode as any as Element).getBoundingClientRect().top
- pageY -= (headerNode as any as Element).getBoundingClientRect().height
- pageY = Math.max(0, pageY)
- ref.current?.scrollToOffset({
- animated: false,
- offset: pageY,
- })
- }
- didAdjustScrollWeb.current = true
- }
- }, [thread])
-
- // On native, we reveal parents in chunks. Although they're all already
- // loaded and FlatList already has its own virtualization, unfortunately FlatList
- // has a bug that causes the content to jump around if too many items are getting
- // prepended at once. It also jumps around if items get prepended during scroll.
- // To work around this, we prepend rows after scroll bumps against the top and rests.
- const needsBumpMaxParents = React.useRef(false)
- const onStartReached = React.useCallback(() => {
- if (skeleton?.parents && maxParents < skeleton.parents.length) {
- needsBumpMaxParents.current = true
- }
- }, [maxParents, skeleton?.parents])
- const bumpMaxParentsIfNeeded = React.useCallback(() => {
- if (!isNative) {
- return
- }
- if (needsBumpMaxParents.current) {
- needsBumpMaxParents.current = false
- setMaxParents(n => n + PARENTS_CHUNK_SIZE)
- }
- }, [])
- const onScrollToTop = bumpMaxParentsIfNeeded
- const onMomentumEnd = React.useCallback(() => {
- 'worklet'
- runOnJS(bumpMaxParentsIfNeeded)()
- }, [bumpMaxParentsIfNeeded])
-
- const onEndReached = React.useCallback(() => {
- if (isFetching || posts.length < maxReplies) return
- setMaxReplies(prev => prev + 50)
- }, [isFetching, maxReplies, posts.length])
-
- const onPostReply = React.useCallback(
- (postUri: string | undefined) => {
- refetch()
- if (postUri) {
- setJustPostedUris(set => {
- const nextSet = new Set(set)
- nextSet.add(postUri)
- return nextSet
- })
- }
- },
- [refetch],
- )
-
- const {openComposer} = useOpenComposer()
- const onReplyToAnchor = React.useCallback(() => {
- if (thread?.type !== 'post') {
- return
- }
- if (anchorPostSource) {
- feedFeedback.sendInteraction({
- item: thread.post.uri,
- event: 'app.bsky.feed.defs#interactionReply',
- feedContext: anchorPostSource.post.feedContext,
- reqId: anchorPostSource.post.reqId,
- })
- }
- openComposer({
- replyTo: {
- uri: thread.post.uri,
- cid: thread.post.cid,
- text: thread.record.text,
- author: thread.post.author,
- embed: thread.post.embed,
- moderation: threadModerationCache.get(thread),
- langs: thread.record.langs,
- },
- onPost: onPostReply,
- })
- }, [
- openComposer,
- thread,
- onPostReply,
- threadModerationCache,
- anchorPostSource,
- feedFeedback,
- ])
-
- const canReply = !error && rootPost && !rootPost.viewer?.replyDisabled
- const hasParents =
- skeleton?.highlightedPost?.type === 'post' &&
- (skeleton.highlightedPost.ctx.isParentLoading ||
- Boolean(skeleton?.parents && skeleton.parents.length > 0))
-
- const renderItem = ({item, index}: {item: RowItem; index: number}) => {
- if (item === REPLY_PROMPT && hasSession) {
- return (
-
- {!isMobile && (
-
- )}
-
- )
- } else if (item === SHOW_HIDDEN_REPLIES || item === SHOW_MUTED_REPLIES) {
- return (
-
- setHiddenRepliesState(
- item === SHOW_HIDDEN_REPLIES
- ? HiddenRepliesState.Show
- : HiddenRepliesState.ShowAndOverridePostHider,
- )
- }
- hideTopBorder={index === 0}
- />
- )
- } else if (isThreadNotFound(item)) {
- return (
-
-
- Deleted post.
-
-
- )
- } else if (isThreadBlocked(item)) {
- return (
-
-
- Blocked post.
-
-
- )
- } else if (isThreadPost(item)) {
- const prev = isThreadPost(posts[index - 1])
- ? (posts[index - 1] as ThreadPost)
- : undefined
- const next = isThreadPost(posts[index + 1])
- ? (posts[index + 1] as ThreadPost)
- : undefined
- const showChildReplyLine = (next?.ctx.depth || 0) > item.ctx.depth
- const showParentReplyLine =
- (item.ctx.depth < 0 && !!item.parent) || item.ctx.depth > 1
- const hasUnrevealedParents =
- index === 0 && skeleton?.parents && maxParents < skeleton.parents.length
-
- if (!treeView && prev && item.ctx.hasMoreSelfThread) {
- return
- }
-
- return (
- setDeferParents(false) : undefined}>
- 0
- }
- onPostReply={onPostReply}
- hideTopBorder={index === 0 && !item.ctx.isParentLoading}
- anchorPostSource={anchorPostSource}
- />
-
- )
- }
- return null
- }
-
- if (!thread || !preferences || error) {
- return (
-
- )
- }
-
- return (
- <>
-
-
-
-
- Post
-
-
-
-
-
-
-
-
-
- }
- initialNumToRender={initialNumToRender}
- windowSize={11}
- sideBorders={false}
- />
-
- {isMobile && canReply && hasSession && (
-
- )}
- >
- )
-}
-
-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 {footerHeight} = useShellLayout()
-
- const animatedStyle = useAnimatedStyle(() => {
- return {
- bottom: footerHeight.get(),
- }
- })
-
- return (
-
-
-
- )
-}
-
-function isThreadPost(v: unknown): v is ThreadPost {
- return !!v && typeof v === 'object' && 'type' in v && v.type === 'post'
-}
-
-function isThreadNotFound(v: unknown): v is ThreadNotFound {
- return !!v && typeof v === 'object' && 'type' in v && v.type === 'not-found'
-}
-
-function isThreadBlocked(v: unknown): v is ThreadBlocked {
- return !!v && typeof v === 'object' && 'type' in v && v.type === 'blocked'
-}
-
-function createThreadSkeleton(
- node: ThreadNode,
- currentDid: string | undefined,
- treeView: boolean,
- modCache: ThreadModerationCache,
- showHiddenReplies: boolean,
- threadgateRecordHiddenReplies: Set,
-): ThreadSkeletonParts | null {
- if (!node) return null
-
- return {
- parents: Array.from(flattenThreadParents(node, !!currentDid)),
- highlightedPost: node,
- replies: Array.from(
- flattenThreadReplies(
- node,
- currentDid,
- treeView,
- modCache,
- showHiddenReplies,
- threadgateRecordHiddenReplies,
- ),
- ),
- }
-}
-
-function* flattenThreadParents(
- node: ThreadNode,
- hasSession: boolean,
-): Generator {
- if (node.type === 'post') {
- if (node.parent) {
- yield* flattenThreadParents(node.parent, hasSession)
- }
- if (!node.ctx.isHighlightedPost) {
- yield node
- }
- } else if (node.type === 'not-found') {
- yield node
- } else if (node.type === 'blocked') {
- yield node
- }
-}
-
-// The enum is ordered to make them easy to merge
-enum HiddenReplyType {
- None = 0,
- Muted = 1,
- Hidden = 2,
-}
-
-function* flattenThreadReplies(
- node: ThreadNode,
- currentDid: string | undefined,
- treeView: boolean,
- modCache: ThreadModerationCache,
- showHiddenReplies: boolean,
- threadgateRecordHiddenReplies: Set,
-): Generator {
- if (node.type === 'post') {
- // dont show pwi-opted-out posts to logged out users
- if (!currentDid && hasPwiOptOut(node)) {
- return HiddenReplyType.None
- }
-
- // handle blurred items
- if (node.ctx.depth > 0) {
- const modui = modCache.get(node)?.ui('contentList')
- if (modui?.blur || modui?.filter) {
- if (!showHiddenReplies || node.ctx.depth > 1) {
- if ((modui.blurs[0] || modui.filters[0]).type === 'muted') {
- return HiddenReplyType.Muted
- }
- return HiddenReplyType.Hidden
- }
- }
-
- if (!showHiddenReplies) {
- const hiddenByThreadgate = threadgateRecordHiddenReplies.has(
- node.post.uri,
- )
- const authorIsViewer = node.post.author.did === currentDid
- if (hiddenByThreadgate && !authorIsViewer) {
- return HiddenReplyType.Hidden
- }
- }
- }
-
- if (!node.ctx.isHighlightedPost) {
- yield node
- }
-
- if (node.replies?.length) {
- let hiddenReplies = HiddenReplyType.None
- for (const reply of node.replies) {
- let hiddenReply = yield* flattenThreadReplies(
- reply,
- currentDid,
- treeView,
- modCache,
- showHiddenReplies,
- threadgateRecordHiddenReplies,
- )
- if (hiddenReply > hiddenReplies) {
- hiddenReplies = hiddenReply
- }
- if (!treeView && !node.ctx.isHighlightedPost) {
- break
- }
- }
-
- // show control to enable hidden replies
- if (node.ctx.depth === 0) {
- if (hiddenReplies === HiddenReplyType.Muted) {
- yield SHOW_MUTED_REPLIES
- } else if (hiddenReplies === HiddenReplyType.Hidden) {
- yield SHOW_HIDDEN_REPLIES
- }
- }
- }
- } else if (node.type === 'not-found') {
- yield node
- } else if (node.type === 'blocked') {
- yield node
- }
- return HiddenReplyType.None
-}
-
-function hasPwiOptOut(node: ThreadPost) {
- return !!node.post.author.labels?.find(l => l.val === '!no-unauthenticated')
-}
-
-function hasBranchingReplies(node?: ThreadNode) {
- if (!node) {
- return false
- }
- if (node.type !== 'post') {
- return false
- }
- if (!node.replies) {
- return false
- }
- if (node.replies.length === 1) {
- return hasBranchingReplies(node.replies[0])
- }
- return true
-}
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
deleted file mode 100644
index 679a506b90..0000000000
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ /dev/null
@@ -1,1036 +0,0 @@
-import {memo, useCallback, useMemo, useState} from 'react'
-import {
- type GestureResponderEvent,
- StyleSheet,
- Text as RNText,
- View,
-} from 'react-native'
-import {
- AppBskyFeedDefs,
- AppBskyFeedPost,
- type AppBskyFeedThreadgate,
- AtUri,
- type ModerationDecision,
- RichText as RichTextAPI,
-} from '@atproto/api'
-import {msg, Plural, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {useActorStatus} from '#/lib/actor-status'
-import {MAX_POST_LINES} from '#/lib/constants'
-import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useTranslate} from '#/lib/hooks/useTranslate'
-import {makeProfileLink} from '#/lib/routes/links'
-import {sanitizeDisplayName} from '#/lib/strings/display-names'
-import {sanitizeHandle} from '#/lib/strings/handles'
-import {countLines} from '#/lib/strings/helpers'
-import {niceDate} from '#/lib/strings/time'
-import {s} from '#/lib/styles'
-import {getTranslatorLink, isPostInLanguage} from '#/locale/helpers'
-import {logger} from '#/logger'
-import {
- POST_TOMBSTONE,
- type Shadow,
- usePostShadow,
-} from '#/state/cache/post-shadow'
-import {useProfileShadow} from '#/state/cache/profile-shadow'
-import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
-import {useLanguagePrefs} from '#/state/preferences'
-import {type ThreadPost} from '#/state/queries/post-thread'
-import {useSession} from '#/state/session'
-import {type OnPostSuccessData} from '#/state/shell/composer'
-import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
-import {type PostSource} from '#/state/unstable-post-source'
-import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn'
-import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
-import {Link} from '#/view/com/util/Link'
-import {formatCount} from '#/view/com/util/numeric/format'
-import {PostMeta} from '#/view/com/util/PostMeta'
-import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
-import {atoms as a, useTheme} from '#/alf'
-import {colors} from '#/components/Admonition'
-import {Button} from '#/components/Button'
-import {useInteractionState} from '#/components/hooks/useInteractionState'
-import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock'
-import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron'
-import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
-import {InlineLinkText} from '#/components/Link'
-import {ContentHider} from '#/components/moderation/ContentHider'
-import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
-import {PostAlerts} from '#/components/moderation/PostAlerts'
-import {PostHider} from '#/components/moderation/PostHider'
-import {type AppModerationCause} from '#/components/Pills'
-import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
-import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
-import {PostControls} from '#/components/PostControls'
-import * as Prompt from '#/components/Prompt'
-import {RichText} from '#/components/RichText'
-import {SubtleWebHover} from '#/components/SubtleWebHover'
-import {Text} from '#/components/Typography'
-import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
-import {WhoCanReply} from '#/components/WhoCanReply'
-import * as bsky from '#/types/bsky'
-
-export function PostThreadItem({
- post,
- record,
- moderation,
- treeView,
- depth,
- prevPost,
- nextPost,
- isHighlightedPost,
- hasMore,
- showChildReplyLine,
- showParentReplyLine,
- hasPrecedingItem,
- overrideBlur,
- onPostReply,
- onPostSuccess,
- hideTopBorder,
- threadgateRecord,
- anchorPostSource,
-}: {
- post: AppBskyFeedDefs.PostView
- record: AppBskyFeedPost.Record
- moderation: ModerationDecision | undefined
- treeView: boolean
- depth: number
- prevPost: ThreadPost | undefined
- nextPost: ThreadPost | undefined
- isHighlightedPost?: boolean
- hasMore?: boolean
- showChildReplyLine?: boolean
- showParentReplyLine?: boolean
- hasPrecedingItem: boolean
- overrideBlur: boolean
- onPostReply: (postUri: string | undefined) => void
- onPostSuccess?: (data: OnPostSuccessData) => void
- hideTopBorder?: boolean
- threadgateRecord?: AppBskyFeedThreadgate.Record
- anchorPostSource?: PostSource
-}) {
- const postShadowed = usePostShadow(post)
- const richText = useMemo(
- () =>
- new RichTextAPI({
- text: record.text,
- facets: record.facets,
- }),
- [record],
- )
- if (postShadowed === POST_TOMBSTONE) {
- return
- }
- if (richText && moderation) {
- return (
-
- )
- }
- return null
-}
-
-function PostThreadItemDeleted({hideTopBorder}: {hideTopBorder?: boolean}) {
- const t = useTheme()
- return (
-
-
-
- This post has been deleted.
-
-
- )
-}
-
-let PostThreadItemLoaded = ({
- post,
- record,
- richText,
- moderation,
- treeView,
- depth,
- prevPost,
- nextPost,
- isHighlightedPost,
- hasMore,
- showChildReplyLine,
- showParentReplyLine,
- hasPrecedingItem,
- overrideBlur,
- onPostReply,
- onPostSuccess,
- hideTopBorder,
- threadgateRecord,
- anchorPostSource,
-}: {
- post: Shadow
- record: AppBskyFeedPost.Record
- richText: RichTextAPI
- moderation: ModerationDecision
- treeView: boolean
- depth: number
- prevPost: ThreadPost | undefined
- nextPost: ThreadPost | undefined
- isHighlightedPost?: boolean
- hasMore?: boolean
- showChildReplyLine?: boolean
- showParentReplyLine?: boolean
- hasPrecedingItem: boolean
- overrideBlur: boolean
- onPostReply: (postUri: string | undefined) => void
- onPostSuccess?: (data: OnPostSuccessData) => void
- hideTopBorder?: boolean
- threadgateRecord?: AppBskyFeedThreadgate.Record
- anchorPostSource?: PostSource
-}): React.ReactNode => {
- const {currentAccount, hasSession} = useSession()
- const feedFeedback = useFeedFeedback(anchorPostSource?.feed, hasSession)
-
- const t = useTheme()
- const pal = usePalette('default')
- const {_, i18n} = useLingui()
- const langPrefs = useLanguagePrefs()
- const {openComposer} = useOpenComposer()
- const [limitLines, setLimitLines] = useState(
- () => countLines(richText?.text) >= MAX_POST_LINES,
- )
- const shadowedPostAuthor = useProfileShadow(post.author)
- const rootUri = record.reply?.root?.uri || post.uri
- const postHref = useMemo(() => {
- const urip = new AtUri(post.uri)
- return makeProfileLink(post.author, 'post', urip.rkey)
- }, [post.uri, post.author])
- const itemTitle = _(msg`Post by ${post.author.handle}`)
- const authorHref = makeProfileLink(post.author)
- const authorTitle = post.author.handle
- const isThreadAuthor = getThreadAuthor(post, record) === currentAccount?.did
- const likesHref = useMemo(() => {
- const urip = new AtUri(post.uri)
- return makeProfileLink(post.author, 'post', urip.rkey, 'liked-by')
- }, [post.uri, post.author])
- const likesTitle = _(msg`Likes on this post`)
- const repostsHref = useMemo(() => {
- const urip = new AtUri(post.uri)
- return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by')
- }, [post.uri, post.author])
- const repostsTitle = _(msg`Reposts of this post`)
- const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({
- threadgateRecord,
- })
- const additionalPostAlerts: AppModerationCause[] = useMemo(() => {
- const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri)
- const isControlledByViewer = new AtUri(rootUri).host === currentAccount?.did
- return isControlledByViewer && isPostHiddenByThreadgate
- ? [
- {
- type: 'reply-hidden',
- source: {type: 'user', did: currentAccount?.did},
- priority: 6,
- },
- ]
- : []
- }, [post, currentAccount?.did, threadgateHiddenReplies, rootUri])
- const quotesHref = useMemo(() => {
- const urip = new AtUri(post.uri)
- return makeProfileLink(post.author, 'post', urip.rkey, 'quotes')
- }, [post.uri, post.author])
- const quotesTitle = _(msg`Quotes of this post`)
- const onlyFollowersCanReply = !!threadgateRecord?.allow?.find(
- rule => rule.$type === 'app.bsky.feed.threadgate#followerRule',
- )
- const showFollowButton =
- currentAccount?.did !== post.author.did && !onlyFollowersCanReply
-
- const needsTranslation = useMemo(
- () =>
- Boolean(
- langPrefs.primaryLanguage &&
- !isPostInLanguage(post, [langPrefs.primaryLanguage]),
- ),
- [post, langPrefs.primaryLanguage],
- )
-
- const onPressReply = () => {
- if (anchorPostSource && isHighlightedPost) {
- feedFeedback.sendInteraction({
- item: post.uri,
- event: 'app.bsky.feed.defs#interactionReply',
- feedContext: anchorPostSource.post.feedContext,
- reqId: anchorPostSource.post.reqId,
- })
- }
- openComposer({
- replyTo: {
- uri: post.uri,
- cid: post.cid,
- text: record.text,
- author: post.author,
- embed: post.embed,
- moderation,
- langs: record.langs,
- },
- onPost: onPostReply,
- onPostSuccess: onPostSuccess,
- })
- }
-
- const onOpenAuthor = () => {
- if (anchorPostSource) {
- feedFeedback.sendInteraction({
- item: post.uri,
- event: 'app.bsky.feed.defs#clickthroughAuthor',
- feedContext: anchorPostSource.post.feedContext,
- reqId: anchorPostSource.post.reqId,
- })
- }
- }
-
- const onOpenEmbed = () => {
- if (anchorPostSource) {
- feedFeedback.sendInteraction({
- item: post.uri,
- event: 'app.bsky.feed.defs#clickthroughEmbed',
- feedContext: anchorPostSource.post.feedContext,
- reqId: anchorPostSource.post.reqId,
- })
- }
- }
-
- const onPressShowMore = useCallback(() => {
- setLimitLines(false)
- }, [setLimitLines])
-
- const {isActive: live} = useActorStatus(post.author)
-
- const reason = anchorPostSource?.post.reason
- const viaRepost = useMemo(() => {
- if (AppBskyFeedDefs.isReasonRepost(reason) && reason.uri && reason.cid) {
- return {
- uri: reason.uri,
- cid: reason.cid,
- }
- }
- }, [reason])
-
- if (!record) {
- return
- }
-
- if (isHighlightedPost) {
- return (
- <>
- {rootUri !== post.uri && (
-
-
-
-
-
- )}
-
-
-
-
-
-
-
-
- {sanitizeDisplayName(
- post.author.displayName ||
- sanitizeHandle(post.author.handle),
- moderation.ui('displayName'),
- )}
-
-
-
-
-
-
-
-
-
- {sanitizeHandle(post.author.handle, '@')}
-
-
-
- {showFollowButton && (
-
-
-
- )}
-
-
-
-
-
- {richText?.text ? (
-
- ) : undefined}
- {post.embed && (
-
-
-
- )}
-
-
- {post.repostCount !== 0 ||
- post.likeCount !== 0 ||
- post.quoteCount !== 0 ? (
- // Show this section unless we're *sure* it has no engagement.
-
- {post.repostCount != null && post.repostCount !== 0 ? (
-
-
-
- {formatCount(i18n, post.repostCount)}
- {' '}
-
-
-
- ) : null}
- {post.quoteCount != null &&
- post.quoteCount !== 0 &&
- !post.viewer?.embeddingDisabled ? (
-
-
-
- {formatCount(i18n, post.quoteCount)}
- {' '}
-
-
-
- ) : null}
- {post.likeCount != null && post.likeCount !== 0 ? (
-
-
-
- {formatCount(i18n, post.likeCount)}
- {' '}
-
-
-
- ) : null}
-
- ) : null}
-
-
-
-
-
-
-
- >
- )
- } else {
- const isThreadedChild = treeView && depth > 0
- const isThreadedChildAdjacentTop =
- isThreadedChild && prevPost?.ctx.depth === depth && depth !== 1
- const isThreadedChildAdjacentBot =
- isThreadedChild && nextPost?.ctx.depth === depth
- return (
-
-
-
-
- {!isThreadedChild && showParentReplyLine && (
-
- )}
-
-
-
-
- {/* If we are in threaded mode, the avatar is rendered in PostMeta */}
- {!isThreadedChild && (
-
-
-
- {showChildReplyLine && (
-
- )}
-
- )}
-
-
-
-
-
- {richText?.text ? (
-
-
- {limitLines && (
-
- )}
-
- ) : undefined}
- {post.embed && (
-
-
-
- )}
-
-
-
- {hasMore ? (
-
-
- More
-
-
-
- ) : undefined}
-
-
- )
- }
-}
-PostThreadItemLoaded = memo(PostThreadItemLoaded)
-
-function PostOuterWrapper({
- post,
- treeView,
- depth,
- showParentReplyLine,
- hasPrecedingItem,
- hideTopBorder,
- children,
-}: React.PropsWithChildren<{
- post: AppBskyFeedDefs.PostView
- treeView: boolean
- depth: number
- showParentReplyLine: boolean
- hasPrecedingItem: boolean
- hideTopBorder?: boolean
-}>) {
- const t = useTheme()
- const {
- state: hover,
- onIn: onHoverIn,
- onOut: onHoverOut,
- } = useInteractionState()
- if (treeView && depth > 0) {
- return (
-
- {Array.from(Array(depth - 1)).map((_, n: number) => (
-
- ))}
-
-
- {children}
-
-
- )
- }
- return (
-
-
- {children}
-
- )
-}
-
-function ExpandedPostDetails({
- post,
- record,
- isThreadAuthor,
- needsTranslation,
-}: {
- post: AppBskyFeedDefs.PostView
- record: AppBskyFeedPost.Record
- isThreadAuthor: boolean
- needsTranslation: boolean
-}) {
- const t = useTheme()
- const pal = usePalette('default')
- const {_, i18n} = useLingui()
- const translate = useTranslate()
- const isRootPost = !('reply' in post.record)
- const langPrefs = useLanguagePrefs()
-
- const onTranslatePress = useCallback(
- (e: GestureResponderEvent) => {
- e.preventDefault()
- translate(record.text || '', langPrefs.primaryLanguage)
-
- if (
- bsky.dangerousIsType(
- post.record,
- AppBskyFeedPost.isRecord,
- )
- ) {
- logger.metric(
- 'translate',
- {
- sourceLanguages: post.record.langs ?? [],
- targetLanguage: langPrefs.primaryLanguage,
- textLength: post.record.text.length,
- },
- {statsig: false},
- )
- }
-
- return false
- },
- [translate, record.text, langPrefs, post],
- )
-
- return (
-
-
-
-
- {niceDate(i18n, post.indexedAt)}
-
- {isRootPost && (
-
- )}
- {needsTranslation && (
- <>
-
- ·
-
-
-
- Translate
-
- >
- )}
-
-
- )
-}
-
-function BackdatedPostIndicator({post}: {post: AppBskyFeedDefs.PostView}) {
- const t = useTheme()
- const {_, i18n} = useLingui()
- const control = Prompt.usePromptControl()
-
- const indexedAt = new Date(post.indexedAt)
- const createdAt = bsky.dangerousIsType(
- post.record,
- AppBskyFeedPost.isRecord,
- )
- ? new Date(post.record.createdAt)
- : new Date(post.indexedAt)
-
- // backdated if createdAt is 24 hours or more before indexedAt
- const isBackdated =
- indexedAt.getTime() - createdAt.getTime() > 24 * 60 * 60 * 1000
-
- if (!isBackdated) return null
-
- const orange = t.name === 'light' ? colors.warning.dark : colors.warning.light
-
- return (
- <>
-
-
-
-
- Archived post
-
-
-
- This post claims to have been created on{' '}
- {niceDate(i18n, createdAt)},
- but was first seen by Bluesky on{' '}
- {niceDate(i18n, indexedAt)}.
-
-
-
-
- Bluesky cannot confirm the authenticity of the claimed date.
-
-
-
- {}} />
-
-
- >
- )
-}
-
-function getThreadAuthor(
- post: AppBskyFeedDefs.PostView,
- record: AppBskyFeedPost.Record,
-): string {
- if (!record.reply) {
- return post.author.did
- }
- try {
- return new AtUri(record.reply.root.uri).host
- } catch {
- return ''
- }
-}
-
-const styles = StyleSheet.create({
- outer: {
- borderTopWidth: StyleSheet.hairlineWidth,
- paddingLeft: 8,
- },
- noTopBorder: {
- borderTopWidth: 0,
- },
- meta: {
- flexDirection: 'row',
- paddingVertical: 2,
- },
- metaExpandedLine1: {
- paddingVertical: 0,
- },
- loadMore: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'flex-start',
- gap: 4,
- paddingHorizontal: 20,
- },
- replyLine: {
- width: 2,
- marginLeft: 'auto',
- marginRight: 'auto',
- },
- cursor: {
- // @ts-ignore web only
- cursor: 'pointer',
- },
-})
diff --git a/src/view/com/post-thread/PostThreadLoadMore.tsx b/src/view/com/post-thread/PostThreadLoadMore.tsx
deleted file mode 100644
index 27e2ea724d..0000000000
--- a/src/view/com/post-thread/PostThreadLoadMore.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import * as React from 'react'
-import {View} from 'react-native'
-import {AppBskyFeedDefs, AtUri} from '@atproto/api'
-import {Trans} from '@lingui/macro'
-
-import {makeProfileLink} from '#/lib/routes/links'
-import {atoms as a, useTheme} from '#/alf'
-import {Text} from '#/components/Typography'
-import {Link} from '../util/Link'
-import {UserAvatar} from '../util/UserAvatar'
-
-export function PostThreadLoadMore({post}: {post: AppBskyFeedDefs.PostView}) {
- const t = useTheme()
-
- const postHref = React.useMemo(() => {
- const urip = new AtUri(post.uri)
- return makeProfileLink(post.author, 'post', urip.rkey)
- }, [post.uri, post.author])
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- Continue thread...
-
-
-
- )
-}
diff --git a/src/view/com/post-thread/PostThreadShowHiddenReplies.tsx b/src/view/com/post-thread/PostThreadShowHiddenReplies.tsx
deleted file mode 100644
index 7dc75520b5..0000000000
--- a/src/view/com/post-thread/PostThreadShowHiddenReplies.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import {View} from 'react-native'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {atoms as a, useTheme} from '#/alf'
-import {Button} from '#/components/Button'
-import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
-import {Text} from '#/components/Typography'
-
-export function PostThreadShowHiddenReplies({
- type,
- onPress,
- hideTopBorder,
-}: {
- type: 'hidden' | 'muted'
- onPress: () => void
- hideTopBorder?: boolean
-}) {
- const {_} = useLingui()
- const t = useTheme()
- const label =
- type === 'muted' ? _(msg`Show muted replies`) : _(msg`Show hidden replies`)
-
- return (
-
- )
-}
diff --git a/src/view/screens/DebugMod.tsx b/src/view/screens/DebugMod.tsx
index d2e492f7e3..f2afe82350 100644
--- a/src/view/screens/DebugMod.tsx
+++ b/src/view/screens/DebugMod.tsx
@@ -31,8 +31,11 @@ import {
groupNotifications,
shouldFilterNotif,
} from '#/state/queries/notifications/util'
+import {threadPost} from '#/state/queries/usePostThread/views'
import {useSession} from '#/state/session'
import {CenteredView, ScrollView} from '#/view/com/util/Views'
+import {ThreadItemAnchor} from '#/screens/PostThread/components/ThreadItemAnchor'
+import {ThreadItemPost} from '#/screens/PostThread/components/ThreadItemPost'
import {ProfileHeaderStandard} from '#/screens/Profile/Header/ProfileHeaderStandard'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -49,7 +52,6 @@ import * as ProfileCard from '#/components/ProfileCard'
import {H1, H3, P, Text} from '#/components/Typography'
import {ScreenHider} from '../../components/moderation/ScreenHider'
import {NotificationFeedItem} from '../com/notifications/NotificationFeedItem'
-import {PostThreadItem} from '../com/post-thread/PostThreadItem'
import {PostFeedItem} from '../com/posts/PostFeedItem'
const LABEL_VALUES: (keyof typeof LABELS)[] = Object.keys(
@@ -519,13 +521,13 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
-
+
>
)}
@@ -837,28 +839,33 @@ function MockPostFeedItem({
function MockPostThreadItem({
post,
- moderation,
- reply,
+ moderationOpts,
+ isReply,
}: {
post: AppBskyFeedDefs.PostView
- moderation: ModerationDecision
- reply?: boolean
+ moderationOpts: ModerationOpts
+ isReply?: boolean
}) {
- return (
- {}}
- />
+ const thread = threadPost({
+ uri: post.uri,
+ depth: isReply ? 1 : 0,
+ value: {
+ $type: 'app.bsky.unspecced.defs#threadItemPost',
+ post,
+ moreParents: false,
+ moreReplies: 0,
+ opThread: false,
+ hiddenByThreadgate: false,
+ mutedByViewer: false,
+ },
+ moderationOpts,
+ threadgateHiddenReplies: new Set(),
+ })
+
+ return isReply ? (
+
+ ) : (
+
)
}
diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx
index cc611e0d63..f07c971fb2 100644
--- a/src/view/screens/PostThread.tsx
+++ b/src/view/screens/PostThread.tsx
@@ -5,17 +5,14 @@ import {
type CommonNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
-import {useGate} from '#/lib/statsig/statsig'
import {makeRecordUri} from '#/lib/strings/url-helpers'
import {useSetMinimalShellMode} from '#/state/shell'
-import {PostThread as PostThreadComponent} from '#/view/com/post-thread/PostThread'
import {PostThread} from '#/screens/PostThread'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps
export function PostThreadScreen({route}: Props) {
const setMinimalShellMode = useSetMinimalShellMode()
- const gate = useGate()
const {name, rkey} = route.params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
@@ -28,11 +25,7 @@ export function PostThreadScreen({route}: Props) {
return (
- {gate('post_threads_v2_unspecced') || __DEV__ ? (
-
- ) : (
-
- )}
+
)
}