From fb4f5709c43c070653c917e3196b9b1c120418a6 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 9 Nov 2023 15:35:25 -0800 Subject: Refactor post threads to use react query (#1851) * Add post and post-thread queries * Update PostThread components to use new queries * Move from normalized cache to shadow cache model * Merge post shadow into the post automatically * Remove dead code * Remove old temporary session * Fix: set agent on session creation * Temporarily double-login * Handle post-thread uri resolution errors --- src/view/com/post-thread/PostThread.tsx | 470 ++++++++++++++++++-------------- 1 file changed, 262 insertions(+), 208 deletions(-) (limited to 'src/view/com/post-thread/PostThread.tsx') diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index f868c3dca..1e85b3e31 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -1,6 +1,4 @@ import React, {useRef} from 'react' -import {runInAction} from 'mobx' -import {observer} from 'mobx-react-lite' import { ActivityIndicator, Pressable, @@ -11,8 +9,6 @@ import { } from 'react-native' import {AppBskyFeedDefs} from '@atproto/api' import {CenteredView, FlatList} from '../util/Views' -import {PostThreadModel} from 'state/models/content/post-thread' -import {PostThreadItemModel} from 'state/models/content/post-thread-item' import { FontAwesomeIcon, FontAwesomeIconStyle, @@ -23,45 +19,36 @@ import {ViewHeader} from '../util/ViewHeader' import {ErrorMessage} from '../util/error/ErrorMessage' import {Text} from '../util/text/Text' import {s} from 'lib/styles' -import {isNative} from 'platform/detection' import {usePalette} from 'lib/hooks/usePalette' import {useSetTitle} from 'lib/hooks/useSetTitle' +import { + ThreadNode, + 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 {logger} from '#/logger' +import {cleanError} from '#/lib/strings/errors' +import {useStores} from '#/state' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -const MAINTAIN_VISIBLE_CONTENT_POSITION = {minIndexForVisible: 2} +// const MAINTAIN_VISIBLE_CONTENT_POSITION = {minIndexForVisible: 2} TODO + +const TOP_COMPONENT = {_reactKey: '__top_component__'} +const PARENT_SPINNER = {_reactKey: '__parent_spinner__'} +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__'} -const TOP_COMPONENT = { - _reactKey: '__top_component__', - _isHighlightedPost: false, -} -const PARENT_SPINNER = { - _reactKey: '__parent_spinner__', - _isHighlightedPost: false, -} -const REPLY_PROMPT = {_reactKey: '__reply__', _isHighlightedPost: false} -const DELETED = {_reactKey: '__deleted__', _isHighlightedPost: false} -const BLOCKED = {_reactKey: '__blocked__', _isHighlightedPost: false} -const CHILD_SPINNER = { - _reactKey: '__child_spinner__', - _isHighlightedPost: false, -} -const LOAD_MORE = { - _reactKey: '__load_more__', - _isHighlightedPost: false, -} -const BOTTOM_COMPONENT = { - _reactKey: '__bottom_component__', - _isHighlightedPost: false, - _showBorder: true, -} type YieldedItem = - | PostThreadItemModel + | ThreadPost | typeof TOP_COMPONENT | typeof PARENT_SPINNER | typeof REPLY_PROMPT @@ -69,66 +56,125 @@ type YieldedItem = | typeof BLOCKED | typeof PARENT_SPINNER -export const PostThread = observer(function PostThread({ +export function PostThread({ uri, - view, onPressReply, treeView, }: { - uri: string - view: PostThreadModel + uri: string | undefined onPressReply: () => void treeView: boolean }) { - const pal = usePalette('default') - const {_} = useLingui() - const {isTablet, isDesktop} = useWebMediaQueries() - const ref = useRef(null) - const hasScrolledIntoView = useRef(false) - const [isRefreshing, setIsRefreshing] = React.useState(false) - const [maxVisible, setMaxVisible] = React.useState(100) - const navigation = useNavigation() - const posts = React.useMemo(() => { - if (view.thread) { - let arr = [TOP_COMPONENT].concat(Array.from(flattenThread(view.thread))) - if (arr.length > maxVisible) { - arr = arr.slice(0, maxVisible).concat([LOAD_MORE]) - } - if (view.isLoadingFromCache) { - if (view.thread?.postRecord?.reply) { - arr.unshift(PARENT_SPINNER) - } - arr.push(CHILD_SPINNER) - } else { - arr.push(BOTTOM_COMPONENT) - } - return arr - } - return [] - }, [view.isLoadingFromCache, view.thread, maxVisible]) - const highlightedPostIndex = posts.findIndex(post => post._isHighlightedPost) + const { + isLoading, + isError, + error, + refetch, + isRefetching, + data: thread, + dataUpdatedAt, + } = usePostThreadQuery(uri) + const rootPost = thread?.type === 'post' ? thread.post : undefined + const rootPostRecord = thread?.type === 'post' ? thread.record : undefined + useSetTitle( - view.thread?.postRecord && + rootPost && `${sanitizeDisplayName( - view.thread.post.author.displayName || - `@${view.thread.post.author.handle}`, - )}: "${view.thread?.postRecord?.text}"`, + rootPost.author.displayName || `@${rootPost.author.handle}`, + )}: "${rootPostRecord?.text}"`, ) - // events - // = + if (isError || AppBskyFeedDefs.isNotFoundPost(thread)) { + return ( + + ) + } + if (AppBskyFeedDefs.isBlockedPost(thread)) { + return + } + if (!thread || isLoading) { + return ( + + + + + + ) + } + return ( + + ) +} + +function PostThreadLoaded({ + thread, + isRefetching, + dataUpdatedAt, + treeView, + onRefresh, + onPressReply, +}: { + thread: ThreadNode + isRefetching: boolean + dataUpdatedAt: number + treeView: boolean + onRefresh: () => void + onPressReply: () => void +}) { + const {_} = useLingui() + const pal = usePalette('default') + const store = useStores() + const {isTablet, isDesktop} = useWebMediaQueries() + const ref = useRef(null) + // const hasScrolledIntoView = useRef(false) TODO + const [maxVisible, setMaxVisible] = React.useState(100) - const onRefresh = React.useCallback(async () => { - setIsRefreshing(true) - try { - view?.refresh() - } catch (err) { - logger.error('Failed to refresh posts thread', {error: err}) + // TODO + // const posts = React.useMemo(() => { + // if (view.thread) { + // let arr = [TOP_COMPONENT].concat(Array.from(flattenThread(view.thread))) + // if (arr.length > maxVisible) { + // arr = arr.slice(0, maxVisible).concat([LOAD_MORE]) + // } + // if (view.isLoadingFromCache) { + // if (view.thread?.postRecord?.reply) { + // arr.unshift(PARENT_SPINNER) + // } + // arr.push(CHILD_SPINNER) + // } else { + // arr.push(BOTTOM_COMPONENT) + // } + // return arr + // } + // return [] + // }, [view.isLoadingFromCache, view.thread, maxVisible]) + // const highlightedPostIndex = posts.findIndex(post => post._isHighlightedPost) + const posts = React.useMemo(() => { + let arr = [TOP_COMPONENT].concat( + Array.from( + flattenThreadSkeleton(sortThread(thread, store.preferences.thread)), + ), + ) + if (arr.length > maxVisible) { + arr = arr.slice(0, maxVisible).concat([LOAD_MORE]) } - setIsRefreshing(false) - }, [view, setIsRefreshing]) + arr.push(BOTTOM_COMPONENT) + return arr + }, [thread, maxVisible, store.preferences.thread]) - const onContentSizeChange = React.useCallback(() => { + // TODO + /*const onContentSizeChange = React.useCallback(() => { // only run once if (hasScrolledIntoView.current) { return @@ -157,7 +203,7 @@ export const PostThread = observer(function PostThread({ view.isFromCache, view.isLoadingFromCache, view.isLoading, - ]) + ])*/ const onScrollToIndexFailed = React.useCallback( (info: { index: number @@ -172,14 +218,6 @@ export const PostThread = observer(function PostThread({ [ref], ) - const onPressBack = React.useCallback(() => { - if (navigation.canGoBack()) { - navigation.goBack() - } else { - navigation.navigate('Home') - } - }, [navigation]) - const renderItem = React.useCallback( ({item, index}: {item: YieldedItem; index: number}) => { if (item === TOP_COMPONENT) { @@ -250,20 +288,27 @@ export const PostThread = observer(function PostThread({ ) - } else if (item instanceof PostThreadItemModel) { - const prev = ( - index - 1 >= 0 ? posts[index - 1] : undefined - ) as PostThreadItemModel + } else if (isThreadPost(item)) { + const prev = isThreadPost(posts[index - 1]) + ? (posts[index - 1] as ThreadPost) + : undefined return ( ) } - return <> + return null }, [ isTablet, @@ -278,75 +323,116 @@ export const PostThread = observer(function PostThread({ posts, onRefresh, treeView, + dataUpdatedAt, _, ], ) - // loading - // = - if ( - !view.hasLoaded || - (view.isLoading && !view.isRefreshing) || - view.params.uri !== uri - ) { - return ( - - - - - - ) - } + return ( + item._reactKey} + renderItem={renderItem} + refreshControl={ + + } + onContentSizeChange={ + undefined //TODOisNative && view.isFromCache ? undefined : onContentSizeChange + } + onScrollToIndexFailed={onScrollToIndexFailed} + style={s.hContentRegion} + // @ts-ignore our .web version only -prf + desktopFixedHeight + /> + ) +} - // error - // = - if (view.hasError) { - if (view.notFound) { - return ( - - - - Post not found - - - The post may have been deleted. - - - - - Back - - - - - ) +function PostThreadBlocked() { + const {_} = useLingui() + const pal = usePalette('default') + const navigation = useNavigation() + + const onPressBack = React.useCallback(() => { + if (navigation.canGoBack()) { + navigation.goBack() + } else { + navigation.navigate('Home') } - return ( - - - - ) - } - if (view.isBlocked) { + }, [navigation]) + + return ( + + + + Post hidden + + + + You have blocked the author or you have been blocked by the author. + + + + + + Back + + + + + ) +} + +function PostThreadError({ + onRefresh, + notFound, + error, +}: { + onRefresh: () => void + notFound: boolean + error: Error | null +}) { + const {_} = useLingui() + const pal = usePalette('default') + const navigation = useNavigation() + + const onPressBack = React.useCallback(() => { + if (navigation.canGoBack()) { + navigation.goBack() + } else { + navigation.navigate('Home') + } + }, [navigation]) + + if (notFound) { return ( - Post hidden + Post not found - - You have blocked the author or you have been blocked by the - author. - + The post may have been deleted. ) } - - // loaded - // = return ( - item._reactKey} - renderItem={renderItem} - refreshControl={ - - } - onContentSizeChange={ - isNative && view.isFromCache ? undefined : onContentSizeChange - } - onScrollToIndexFailed={onScrollToIndexFailed} - style={s.hContentRegion} - // @ts-ignore our .web version only -prf - desktopFixedHeight - /> + + + ) -}) +} + +function isThreadPost(v: unknown): v is ThreadPost { + return !!v && typeof v === 'object' && 'type' in v && v.type === 'post' +} -function* flattenThread( - post: PostThreadItemModel, - isAscending = false, +function* flattenThreadSkeleton( + node: ThreadNode, ): Generator { - if (post.parent) { - if (AppBskyFeedDefs.isNotFoundPost(post.parent)) { - yield DELETED - } else if (AppBskyFeedDefs.isBlockedPost(post.parent)) { - yield BLOCKED - } else { - yield* flattenThread(post.parent as PostThreadItemModel, true) + if (node.type === 'post') { + if (node.parent) { + yield* flattenThreadSkeleton(node.parent) } - } - yield post - if (post._isHighlightedPost) { - yield REPLY_PROMPT - } - if (post.replies?.length) { - for (const reply of post.replies) { - if (AppBskyFeedDefs.isNotFoundPost(reply)) { - yield DELETED - } else { - yield* flattenThread(reply as PostThreadItemModel) + yield node + if (node.ctx.isHighlightedPost) { + yield REPLY_PROMPT + } + if (node.replies?.length) { + for (const reply of node.replies) { + yield* flattenThreadSkeleton(reply) } } - } else if (!isAscending && !post.parent && post.post.replyCount) { - runInAction(() => { - post._hasMore = true - }) + } else if (node.type === 'not-found') { + yield DELETED + } else if (node.type === 'blocked') { + yield BLOCKED } } -- cgit 1.4.1