import {useCallback, useMemo, useRef, useState} from 'react' import {StyleSheet, useWindowDimensions, View} from 'react-native' import {useFocusEffect} from '@react-navigation/native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {AppBskyFeedDefs} from '@atproto/api' import {isNative} from '#/platform/detection' import {cleanError} from '#/lib/strings/errors' import {HITSLOP_10} from '#/lib/constants' import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' import {makeRecordUri} from '#/lib/strings/url-helpers' import {useSetMinimalShellMode} from '#/state/shell' import {PostThread as PostThreadComponent} from '#/view/com/post-thread/PostThread' import * as Layout from '#/components/Layout' import {useSession} from '#/state/session' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useGetPostThreadV2, Slice, HiddenReplyKind} from '#/state/queries/useGetPostThreadV2' import * as Menu from '#/components/Menu' import {Button, ButtonIcon} from '#/components/Button' import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from '#/components/icons/SettingsSlider' import {Text} from '#/components/Typography' import {ScrollProvider} from '#/lib/ScrollContext' import {List, ListMethods} from '#/view/com/util/List' import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' import {PostThreadItem} from '#/view/com/post-thread/PostThreadItem' import {PostThreadComposePrompt} from '#/view/com/post-thread/PostThreadComposePrompt' import {usePreferencesQuery} from '#/state/queries/preferences' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {PostThreadShowHiddenReplies} from '#/view/com/post-thread/PostThreadShowHiddenReplies' 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: 1, } type Props = NativeStackScreenProps export function PostThreadScreen({route}: Props) { const setMinimalShellMode = useSetMinimalShellMode() const {name, rkey} = route.params const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) useFocusEffect( useCallback(() => { setMinimalShellMode(false) }, [setMinimalShellMode]), ) return ( {/* */} ) } function useThreadPreferences() { const {data: preferences} = usePreferencesQuery() const nextThreadPreferences = preferences?.threadViewPrefs /* * Create local state representations of server state */ const [sortReplies, setSortReplies] = useState( nextThreadPreferences?.sort ?? 'hotness', ) const [prioritizeFollowedUsers, setPrioritizeFollowedUsers] = useState( !!nextThreadPreferences?.prioritizeFollowedUsers, ) const [treeViewEnabled, setTreeViewEnabled] = useState( !!nextThreadPreferences?.lab_treeViewEnabled, ) /** * Cache existing and if we get a server update, reset local state */ const [prevServerPrefs, setPrevServerPrefs] = useState(nextThreadPreferences) if (nextThreadPreferences && prevServerPrefs !== nextThreadPreferences) { setPrevServerPrefs(nextThreadPreferences) /* * Reset */ setSortReplies(nextThreadPreferences.sort) setPrioritizeFollowedUsers(nextThreadPreferences.prioritizeFollowedUsers) setTreeViewEnabled(!!nextThreadPreferences.lab_treeViewEnabled) } const isLoaded = !!prevServerPrefs return useMemo(() => ({ isLoaded, sortReplies, setSortReplies, prioritizeFollowedUsers, setPrioritizeFollowedUsers, treeViewEnabled, setTreeViewEnabled, }), [ isLoaded, sortReplies, setSortReplies, prioritizeFollowedUsers, setPrioritizeFollowedUsers, treeViewEnabled, setTreeViewEnabled, ]) } export function Inner({uri}: {uri: string | undefined}) { const t = useTheme() const {_} = useLingui() const {gtPhone} = useBreakpoints() // const {hasSession, currentAccount} = useSession() const initialNumToRender = useInitialNumToRender() const {height: windowHeight} = useWindowDimensions() const { isLoaded: isThreadPreferencesLoaded, sortReplies, setSortReplies, prioritizeFollowedUsers, treeViewEnabled, setTreeViewEnabled, } = useThreadPreferences() const [showHiddenReplies, setShowHiddenReplies] = useState(null) const [shownHiddenReplyKinds, setShownHiddenReplyKinds] = useState>(new Set()) const {isFetching, isPlaceholderData, error, data, refetch} = useGetPostThreadV2({ uri, enabled: isThreadPreferencesLoaded, params: { sort: sortReplies, view: treeViewEnabled ? 'tree' : 'linear', prioritizeFollows: prioritizeFollowedUsers, }, state: { shownHiddenReplyKinds, } }) const ref = useRef(null) const layoutHeaderRef = useRef(null) const anchorPostRef = useRef(null) // TODO const [justPostedUris, setJustPostedUris] = useState( () => new Set(), ) const onPostReply = useCallback( (postUri: string | undefined) => { refetch() if (postUri) { setJustPostedUris(set => { const nextSet = new Set(set) nextSet.add(postUri) return nextSet }) } }, [refetch], ) const optimisticOnPostReply = ({ post, }: { post: AppBskyFeedDefs.ThreadItemPost, }) => (_: any, posts: AppBskyFeedDefs.ThreadItemPost[]) => { const parentDepth = post.depth if (posts.length) { for (let i = 0; i < posts.length; i++) { const p = posts[i] p.depth = parentDepth + 1 + i } } } const {openComposer} = useOpenComposer() const onReplyToAnchor = () => { const anchorPost = data?.slices.find(slice => slice.type === 'threadSlice' && slice.ui.isAnchor) if (anchorPost?.type !== 'threadSlice') { return } const post = anchorPost.slice.post openComposer({ replyTo: { uri: anchorPost.slice.uri, cid: post.cid, text: post.record.text, author: post.author, embed: post.embed, moderation: anchorPost.moderation, }, onPost: optimisticOnPostReply({post:anchorPost.slice}), }) } const renderItem = ({item, index}: {item: Slice; index: number}) => { if (item.type === 'threadSlice') { return ( 0 } onPostReply={optimisticOnPostReply({post:item.slice})} hideTopBorder={index === 0} // && !item.slice.isParentLoading} // TODO /> ) } else if (item.type === 'threadSliceBlocked') { return ( Blocked post. ) } else if (item.type === 'threadSliceNotFound') { return ( Deleted post. ) } else if (item.type === 'replyComposer') { return ( {gtPhone && } ) } else if (item.type === 'showHiddenReplies') { return ( setShownHiddenReplyKinds(kinds => new Set([...kinds, item.kind]))} /> ) } return null } /* * This is only used on the web to keep the post in view when its parents * load. On native, we rely on `maintainVisibleContentPosition` instead. * * This runs synchronously before layout, meaning by the time the page * paints, we've already measured and adjusted the scroll position of the * list. */ const didAdjustScrollWeb = useRef(false) const onContentSizeChangeWeb = () => { // only run once if (didAdjustScrollWeb.current) return if (!isPlaceholderData) { // Measure synchronously to avoid a layout jump. const anchorPost = anchorPostRef.current as any as Element const headerNode = layoutHeaderRef.current as any as Element if (anchorPost && headerNode) { // get new scroll position let pageY = anchorPost.getBoundingClientRect().top // subtract header height pageY -= headerNode.getBoundingClientRect().height // don't scroll past 0 pageY = Math.max(0, pageY) ref.current?.scrollToOffset({ animated: false, offset: pageY, }) didAdjustScrollWeb.current = true } } } return ( <> Post {error ? ( ) : ( } initialNumToRender={initialNumToRender} windowSize={11} sideBorders={false} /> )} ) } function PostThreadError({error}: {error: Error}) { const {_} = useLingui() // TODO use new cleanError hook const {title, message} = useMemo(() => { let title = _(msg`An error occurred`) let message = cleanError(error) if (error.message.startsWith('Post not found')) { title = _(msg`Post not found`) message = _(msg`The post may have been deleted.`) } return {title, message} }, [_, error]) return } function 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") ) } const keyExtractor = (item: Slice) => { return item.key }