diff options
Diffstat (limited to 'src/view/com/post-thread/PostThread.tsx')
-rw-r--r-- | src/view/com/post-thread/PostThread.tsx | 675 |
1 files changed, 313 insertions, 362 deletions
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index ef3d7e2b6..c1159379d 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -1,66 +1,68 @@ import React, {useEffect, useRef} from 'react' -import { - ActivityIndicator, - Pressable, - StyleSheet, - TouchableOpacity, - View, -} from 'react-native' +import {StyleSheet, useWindowDimensions, View} from 'react-native' import {AppBskyFeedDefs} from '@atproto/api' -import {CenteredView} from '../util/Views' -import {LoadingScreen} from '../util/LoadingScreen' -import {List, ListMethods} from '../util/List' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {PostThreadItem} from './PostThreadItem' -import {ComposePrompt} from '../composer/Prompt' -import {ViewHeader} from '../util/ViewHeader' -import {ErrorMessage} from '../util/error/ErrorMessage' -import {Text} from '../util/text/Text' -import {s} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' -import {useSetTitle} from 'lib/hooks/useSetTitle' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' +import {isAndroid, isNative, isWeb} from '#/platform/detection' import { + sortThread, + ThreadBlocked, ThreadNode, + ThreadNotFound, ThreadPost, usePostThreadQuery, - sortThread, } from '#/state/queries/post-thread' -import {useNavigation} from '@react-navigation/native' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {NavigationProp} from 'lib/routes/types' -import {sanitizeDisplayName} from 'lib/strings/display-names' -import {cleanError} from '#/lib/strings/errors' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' import { - UsePreferencesQueryResponse, useModerationOpts, usePreferencesQuery, } from '#/state/queries/preferences' import {useSession} from '#/state/session' -import {isAndroid, isNative} from '#/platform/detection' -import {logger} from '#/logger' -import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' +import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender' +import {usePalette} from 'lib/hooks/usePalette' +import {useSetTitle} from 'lib/hooks/useSetTitle' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {sanitizeDisplayName} from 'lib/strings/display-names' +import {cleanError} from 'lib/strings/errors' +import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' +import {ComposePrompt} from '../composer/Prompt' +import {List, ListMethods} from '../util/List' +import {Text} from '../util/text/Text' +import {ViewHeader} from '../util/ViewHeader' +import {PostThreadItem} from './PostThreadItem' + +// 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 = {minIndexForVisible: 1} +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 TOP_COMPONENT = {_reactKey: '__top_component__'} const REPLY_PROMPT = {_reactKey: '__reply__'} -const DELETED = {_reactKey: '__deleted__'} -const BLOCKED = {_reactKey: '__blocked__'} -const CHILD_SPINNER = {_reactKey: '__child_spinner__'} const LOAD_MORE = {_reactKey: '__load_more__'} -const BOTTOM_COMPONENT = {_reactKey: '__bottom_component__'} -type YieldedItem = - | ThreadPost +type YieldedItem = ThreadPost | ThreadBlocked | ThreadNotFound +type RowItem = + | YieldedItem + // TODO: TS doesn't actually enforce it's one of these, it only enforces matching shape. | typeof TOP_COMPONENT | typeof REPLY_PROMPT - | typeof DELETED - | typeof BLOCKED + | typeof LOAD_MORE + +type ThreadSkeletonParts = { + parents: YieldedItem[] + highlightedPost: ThreadNode + replies: YieldedItem[] +} + +const keyExtractor = (item: RowItem) => { + return item._reactKey +} export function PostThread({ uri, @@ -69,17 +71,30 @@ export function PostThread({ }: { uri: string | undefined onCanReply: (canReply: boolean) => void - onPressReply: () => void + onPressReply: () => unknown }) { + const {hasSession} = useSession() + const {_} = useLingui() + const pal = usePalette('default') + const {isMobile, isTabletOrMobile} = useWebMediaQueries() + const initialNumToRender = useInitialNumToRender() + const {height: windowHeight} = useWindowDimensions() + + const {data: preferences} = usePreferencesQuery() const { - isLoading, - isError, - error, + isFetching, + isError: isThreadError, + error: threadError, refetch, data: thread, } = usePostThreadQuery(uri) - const {data: preferences} = usePreferencesQuery() + const treeView = React.useMemo( + () => + !!preferences?.threadViewPrefs?.lab_treeViewEnabled && + hasBranchingReplies(thread), + [preferences?.threadViewPrefs, thread], + ) const rootPost = thread?.type === 'post' ? thread.post : undefined const rootPostRecord = thread?.type === 'post' ? thread.record : undefined @@ -89,14 +104,23 @@ export function PostThread({ rootPost && moderationOpts ? moderatePost(rootPost, moderationOpts) : undefined - - const cause = mod?.content.cause - - return cause - ? cause.type === 'label' && cause.labelDef.id === '!no-unauthenticated' - : false + 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<ListMethods>(null) + const highlightedPostRef = useRef<View | null>(null) + const [maxParents, setMaxParents] = React.useState( + isWeb ? Infinity : PARENTS_CHUNK_SIZE, + ) + const [maxReplies, setMaxReplies] = React.useState(50) + useSetTitle( rootPost && !isNoPwi ? `${sanitizeDisplayName( @@ -104,147 +128,164 @@ export function PostThread({ )}: "${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 skeleton = React.useMemo(() => { + const threadViewPrefs = preferences?.threadViewPrefs + if (!threadViewPrefs || !thread) return null + + return createThreadSkeleton( + sortThread(thread, threadViewPrefs), + hasSession, + treeView, + ) + }, [thread, preferences?.threadViewPrefs, hasSession, treeView]) + + 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]) + useEffect(() => { - if (rootPost) { + if (error) { + onCanReply(false) + } else if (rootPost) { onCanReply(!rootPost.viewer?.replyDisabled) } - }, [rootPost, onCanReply]) - - if (isError || AppBskyFeedDefs.isNotFoundPost(thread)) { - return ( - <PostThreadError - error={error} - notFound={AppBskyFeedDefs.isNotFoundPost(thread)} - onRefresh={refetch} - /> - ) - } - if (AppBskyFeedDefs.isBlockedPost(thread)) { - return <PostThreadBlocked /> - } - if (!thread || isLoading || !preferences) { - return <LoadingScreen /> - } - return ( - <PostThreadLoaded - thread={thread} - threadViewPrefs={preferences.threadViewPrefs} - onRefresh={refetch} - onPressReply={onPressReply} - /> - ) -} - -function PostThreadLoaded({ - thread, - threadViewPrefs, - onRefresh, - onPressReply, -}: { - thread: ThreadNode - threadViewPrefs: UsePreferencesQueryResponse['threadViewPrefs'] - onRefresh: () => void - onPressReply: () => void -}) { - const {hasSession} = useSession() - const {_} = useLingui() - const pal = usePalette('default') - const {isMobile, isTabletOrMobile} = useWebMediaQueries() - const ref = useRef<ListMethods>(null) - const highlightedPostRef = useRef<View | null>(null) - const needsScrollAdjustment = useRef<boolean>( - !isNative || // web always uses scroll adjustment - (thread.type === 'post' && !thread.ctx.isParentLoading), // native only does it when not loading from placeholder - ) - const [maxVisible, setMaxVisible] = React.useState(100) - const [isPTRing, setIsPTRing] = React.useState(false) - const treeView = React.useMemo( - () => !!threadViewPrefs.lab_treeViewEnabled && hasBranchingReplies(thread), - [threadViewPrefs, thread], - ) + }, [rootPost, onCanReply, error]) // construct content const posts = React.useMemo(() => { - let arr = Array.from( - flattenThreadSkeleton( - sortThread(thread, threadViewPrefs), - hasSession, - treeView, - ), - ) - if (arr.length > maxVisible) { - arr = arr.slice(0, maxVisible).concat([LOAD_MORE]) - } - if (arr.indexOf(CHILD_SPINNER) === -1) { - arr.push(BOTTOM_COMPONENT) + if (!skeleton) return [] + + const {parents, highlightedPost, replies} = skeleton + let arr: RowItem[] = [] + if (highlightedPost.type === 'post') { + const isRoot = + !highlightedPost.parent && !highlightedPost.ctx.isParentLoading + if (isRoot) { + // No parents to load. + arr.push(TOP_COMPONENT) + } else { + if (highlightedPost.ctx.isParentLoading || deferParents) { + // We're loading parents of the highlighted post. + // In this case, we don't render anything above the post. + // 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. + } else { + // Everything is loaded + let startIndex = Math.max(0, parents.length - maxParents) + if (startIndex === 0) { + arr.push(TOP_COMPONENT) + } else { + // 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/www.mozzius.dev/post/3kjqhblh6qk2o + } + 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 - }, [thread, treeView, maxVisible, threadViewPrefs, hasSession]) - - /** - * NOTE - * Scroll positioning - * - * This callback is run if needsScrollAdjustment.current == true, which is... - * - On web: always - * - On native: when the placeholder cache is not being used - * - * It then only runs when viewing a reply, and the goal is to scroll the - * reply into view. - * - * On native, if the placeholder cache is being used then maintainVisibleContentPosition - * is a more effective solution, so we use that. Otherwise, typically we're loading from - * the react-query cache, so we just need to immediately scroll down to the post. - * - * On desktop, maintainVisibleContentPosition isn't supported so we just always use - * this technique. - * - * -prf - */ - const onContentSizeChange = React.useCallback(() => { + }, [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<boolean>(false) + const onContentSizeChangeWeb = React.useCallback(() => { // only run once - if (!needsScrollAdjustment.current) { + if (didAdjustScrollWeb.current) { return } - // wait for loading to finish - if (thread.type === 'post' && !!thread.parent) { + if (thread?.type === 'post' && !!thread.parent) { function onMeasure(pageY: number) { ref.current?.scrollToOffset({ animated: false, offset: pageY, }) } - if (isNative) { - highlightedPostRef.current?.measure( - (_x, _y, _width, _height, _pageX, pageY) => { - onMeasure(pageY) - }, - ) - } else { - // Measure synchronously to avoid a layout jump. - const domNode = highlightedPostRef.current - if (domNode) { - const pageY = (domNode as any as Element).getBoundingClientRect().top - onMeasure(pageY) - } + // Measure synchronously to avoid a layout jump. + const domNode = highlightedPostRef.current + if (domNode) { + const pageY = (domNode as any as Element).getBoundingClientRect().top + onMeasure(pageY) } - needsScrollAdjustment.current = false + didAdjustScrollWeb.current = true } }, [thread]) - const onPTR = React.useCallback(async () => { - setIsPTRing(true) - try { - await onRefresh() - } catch (err) { - logger.error('Failed to refresh posts thread', {message: err}) + // 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 } - setIsPTRing(false) - }, [setIsPTRing, onRefresh]) + }, [maxParents, skeleton?.parents]) + const bumpMaxParentsIfNeeded = React.useCallback(() => { + if (!isNative) { + return + } + if (needsBumpMaxParents.current) { + needsBumpMaxParents.current = false + setMaxParents(n => n + PARENTS_CHUNK_SIZE) + } + }, []) + const onMomentumScrollEnd = bumpMaxParentsIfNeeded + const onScrollToTop = bumpMaxParentsIfNeeded + + const onEndReached = React.useCallback(() => { + if (isFetching || posts.length < maxReplies) return + setMaxReplies(prev => prev + 50) + }, [isFetching, maxReplies, posts.length]) const renderItem = React.useCallback( - ({item, index}: {item: YieldedItem; index: number}) => { + ({item, index}: {item: RowItem; index: number}) => { if (item === TOP_COMPONENT) { return isTabletOrMobile ? ( <ViewHeader @@ -257,7 +298,7 @@ function PostThreadLoaded({ {!isMobile && <ComposePrompt onPressCompose={onPressReply} />} </View> ) - } else if (item === DELETED) { + } else if (isThreadNotFound(item)) { return ( <View style={[pal.border, pal.viewLight, styles.itemContainer]}> <Text type="lg-bold" style={pal.textLight}> @@ -265,7 +306,7 @@ function PostThreadLoaded({ </Text> </View> ) - } else if (item === BLOCKED) { + } else if (isThreadBlocked(item)) { return ( <View style={[pal.border, pal.viewLight, styles.itemContainer]}> <Text type="lg-bold" style={pal.textLight}> @@ -273,46 +314,6 @@ function PostThreadLoaded({ </Text> </View> ) - } else if (item === LOAD_MORE) { - return ( - <Pressable - onPress={() => setMaxVisible(n => n + 50)} - style={[pal.border, pal.view, styles.itemContainer]} - accessibilityLabel={_(msg`Load more posts`)} - accessibilityHint=""> - <View - style={[ - pal.viewLight, - {paddingHorizontal: 18, paddingVertical: 14, borderRadius: 6}, - ]}> - <Text type="lg-medium" style={pal.text}> - <Trans>Load more posts</Trans> - </Text> - </View> - </Pressable> - ) - } else if (item === BOTTOM_COMPONENT) { - // HACK - // 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 - // @ts-ignore web-only - style={{ - // Leave enough space below that the scroll doesn't jump - height: isNative ? 600 : '100vh', - borderTopWidth: 1, - borderColor: pal.colors.border, - }} - /> - ) - } else if (item === CHILD_SPINNER) { - return ( - <View style={[pal.border, styles.childSpinner]}> - <ActivityIndicator /> - </View> - ) } else if (isThreadPost(item)) { const prev = isThreadPost(posts[index - 1]) ? (posts[index - 1] as ThreadPost) @@ -320,9 +321,14 @@ function PostThreadLoaded({ const next = isThreadPost(posts[index - 1]) ? (posts[index - 1] as ThreadPost) : undefined + const hasUnrevealedParents = + index === 0 && + skeleton?.parents && + maxParents < skeleton.parents.length return ( <View - ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}> + ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined} + onLayout={deferParents ? () => setDeferParents(false) : undefined}> <PostThreadItem post={item.post} record={item.record} @@ -334,8 +340,10 @@ function PostThreadLoaded({ hasMore={item.ctx.hasMore} showChildReplyLine={item.ctx.showChildReplyLine} showParentReplyLine={item.ctx.showParentReplyLine} - hasPrecedingItem={!!prev?.ctx.showChildReplyLine} - onPostReply={onRefresh} + hasPrecedingItem={ + !!prev?.ctx.showChildReplyLine || !!hasUnrevealedParents + } + onPostReply={refetch} /> </View> ) @@ -345,182 +353,133 @@ function PostThreadLoaded({ [ hasSession, isTabletOrMobile, + _, isMobile, onPressReply, pal.border, pal.viewLight, pal.textLight, - pal.view, - pal.text, - pal.colors.border, posts, - onRefresh, + skeleton?.parents, + maxParents, + deferParents, treeView, - _, + refetch, ], ) return ( - <List - ref={ref} - data={posts} - initialNumToRender={!isNative ? posts.length : undefined} - maintainVisibleContentPosition={ - !needsScrollAdjustment.current - ? MAINTAIN_VISIBLE_CONTENT_POSITION - : undefined - } - keyExtractor={item => item._reactKey} - renderItem={renderItem} - refreshing={isPTRing} - onRefresh={onPTR} - onContentSizeChange={onContentSizeChange} - style={s.hContentRegion} - // @ts-ignore our .web version only -prf - desktopFixedHeight - removeClippedSubviews={isAndroid ? false : undefined} - /> + <> + <ListMaybePlaceholder + isLoading={(!preferences || !thread) && !error} + isError={!!error} + onRetry={refetch} + errorTitle={error?.title} + errorMessage={error?.message} + /> + {!error && thread && ( + <List + ref={ref} + data={posts} + renderItem={renderItem} + keyExtractor={keyExtractor} + onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb} + onStartReached={onStartReached} + onEndReached={onEndReached} + onEndReachedThreshold={2} + onMomentumScrollEnd={onMomentumScrollEnd} + onScrollToTop={onScrollToTop} + maintainVisibleContentPosition={ + isNative ? MAINTAIN_VISIBLE_CONTENT_POSITION : undefined + } + // @ts-ignore our .web version only -prf + desktopFixedHeight + removeClippedSubviews={isAndroid ? false : undefined} + ListFooterComponent={ + <ListFooter + isFetching={isFetching} + onRetry={refetch} + // 300 is based on the minimum height of a post. This is enough extra height for the `maintainVisPos` to + // work without causing weird jumps on web or glitches on native + height={windowHeight - 200} + /> + } + initialNumToRender={initialNumToRender} + windowSize={11} + /> + )} + </> ) } -function PostThreadBlocked() { - const {_} = useLingui() - const pal = usePalette('default') - const navigation = useNavigation<NavigationProp>() +function isThreadPost(v: unknown): v is ThreadPost { + return !!v && typeof v === 'object' && 'type' in v && v.type === 'post' +} - const onPressBack = React.useCallback(() => { - if (navigation.canGoBack()) { - navigation.goBack() - } else { - navigation.navigate('Home') - } - }, [navigation]) +function isThreadNotFound(v: unknown): v is ThreadNotFound { + return !!v && typeof v === 'object' && 'type' in v && v.type === 'not-found' +} - return ( - <CenteredView> - <View style={[pal.view, pal.border, styles.notFoundContainer]}> - <Text type="title-lg" style={[pal.text, s.mb5]}> - <Trans>Post hidden</Trans> - </Text> - <Text type="md" style={[pal.text, s.mb10]}> - <Trans> - You have blocked the author or you have been blocked by the author. - </Trans> - </Text> - <TouchableOpacity - onPress={onPressBack} - accessibilityRole="button" - accessibilityLabel={_(msg`Back`)} - accessibilityHint=""> - <Text type="2xl" style={pal.link}> - <FontAwesomeIcon - icon="angle-left" - style={[pal.link as FontAwesomeIconStyle, s.mr5]} - size={14} - /> - <Trans context="action">Back</Trans> - </Text> - </TouchableOpacity> - </View> - </CenteredView> - ) +function isThreadBlocked(v: unknown): v is ThreadBlocked { + return !!v && typeof v === 'object' && 'type' in v && v.type === 'blocked' } -function PostThreadError({ - onRefresh, - notFound, - error, -}: { - onRefresh: () => void - notFound: boolean - error: Error | null -}) { - const {_} = useLingui() - const pal = usePalette('default') - const navigation = useNavigation<NavigationProp>() +function createThreadSkeleton( + node: ThreadNode, + hasSession: boolean, + treeView: boolean, +): ThreadSkeletonParts | null { + if (!node) return null - const onPressBack = React.useCallback(() => { - if (navigation.canGoBack()) { - navigation.goBack() - } else { - navigation.navigate('Home') - } - }, [navigation]) - - if (notFound) { - return ( - <CenteredView> - <View style={[pal.view, pal.border, styles.notFoundContainer]}> - <Text type="title-lg" style={[pal.text, s.mb5]}> - <Trans>Post not found</Trans> - </Text> - <Text type="md" style={[pal.text, s.mb10]}> - <Trans>The post may have been deleted.</Trans> - </Text> - <TouchableOpacity - onPress={onPressBack} - accessibilityRole="button" - accessibilityLabel={_(msg`Back`)} - accessibilityHint=""> - <Text type="2xl" style={pal.link}> - <FontAwesomeIcon - icon="angle-left" - style={[pal.link as FontAwesomeIconStyle, s.mr5]} - size={14} - /> - <Trans>Back</Trans> - </Text> - </TouchableOpacity> - </View> - </CenteredView> - ) + return { + parents: Array.from(flattenThreadParents(node, hasSession)), + highlightedPost: node, + replies: Array.from(flattenThreadReplies(node, hasSession, treeView)), } - return ( - <CenteredView> - <ErrorMessage message={cleanError(error)} onPressTryAgain={onRefresh} /> - </CenteredView> - ) } -function isThreadPost(v: unknown): v is ThreadPost { - return !!v && typeof v === 'object' && 'type' in v && v.type === 'post' +function* flattenThreadParents( + node: ThreadNode, + hasSession: boolean, +): Generator<YieldedItem, void> { + 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 + } } -function* flattenThreadSkeleton( +function* flattenThreadReplies( node: ThreadNode, hasSession: boolean, treeView: boolean, - isTraversingReplies: boolean = false, ): Generator<YieldedItem, void> { if (node.type === 'post') { - if (!node.ctx.isParentLoading) { - if (node.parent) { - yield* flattenThreadSkeleton(node.parent, hasSession, treeView, false) - } else if (!isTraversingReplies) { - yield TOP_COMPONENT - } - } - if (!hasSession && node.ctx.depth > 0 && hasPwiOptOut(node)) { + if (!hasSession && hasPwiOptOut(node)) { return } - yield node - if (node.ctx.isHighlightedPost && !node.post.viewer?.replyDisabled) { - yield REPLY_PROMPT + if (!node.ctx.isHighlightedPost) { + yield node } if (node.replies?.length) { for (const reply of node.replies) { - yield* flattenThreadSkeleton(reply, hasSession, treeView, true) + yield* flattenThreadReplies(reply, hasSession, treeView) if (!treeView && !node.ctx.isHighlightedPost) { break } } - } else if (node.ctx.isChildLoading) { - yield CHILD_SPINNER } } else if (node.type === 'not-found') { - yield DELETED + yield node } else if (node.type === 'blocked') { - yield BLOCKED + yield node } } @@ -528,7 +487,10 @@ function hasPwiOptOut(node: ThreadPost) { return !!node.post.author.labels?.find(l => l.val === '!no-unauthenticated') } -function hasBranchingReplies(node: ThreadNode) { +function hasBranchingReplies(node?: ThreadNode) { + if (!node) { + return false + } if (node.type !== 'post') { return false } @@ -542,20 +504,9 @@ function hasBranchingReplies(node: ThreadNode) { } const styles = StyleSheet.create({ - notFoundContainer: { - margin: 10, - paddingHorizontal: 18, - paddingVertical: 14, - borderRadius: 6, - }, itemContainer: { borderTopWidth: 1, paddingHorizontal: 18, paddingVertical: 18, }, - childSpinner: { - borderTopWidth: 1, - paddingTop: 40, - paddingBottom: 200, - }, }) |