From b9d7c5a5c48428225d22574fe1da8e607fed81d0 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 6 Jun 2025 16:35:38 -0500 Subject: [PATCH] Clean up callbacks, better error state --- .../PostThread/components/ThreadError.tsx | 90 ++++++ src/screens/PostThread/index.tsx | 264 +++++++++--------- 2 files changed, 221 insertions(+), 133 deletions(-) create mode 100644 src/screens/PostThread/components/ThreadError.tsx diff --git a/src/screens/PostThread/components/ThreadError.tsx b/src/screens/PostThread/components/ThreadError.tsx new file mode 100644 index 0000000000..3e15e890b6 --- /dev/null +++ b/src/screens/PostThread/components/ThreadError.tsx @@ -0,0 +1,90 @@ +import {useMemo} from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useCleanError} from '#/lib/hooks/useCleanError' +import {OUTER_SPACE} from '#/screens/PostThread/const' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotateCounterClockwise' +import * as Layout from '#/components/Layout' +import {Text} from '#/components/Typography' + +export function ThreadError({ + error, + onRetry, +}: { + error: Error + onRetry: () => void +}) { + const t = useTheme() + const {_} = useLingui() + const cleanError = useCleanError() + + // TODO use new cleanError hook + const {title, message} = useMemo(() => { + let title = _(msg`Error loading post`) + let message = _(msg`Something went wrong. Please try again in a moment.`) + + const {raw, clean} = cleanError(error) + + if (error.message.startsWith('Post not found')) { + title = _(msg`Post not found`) + message = clean || raw || message + } + + return {title, message} + }, [_, error, cleanError]) + + return ( + + + + + + {title} + + + {message} + + + + + + + ) +} diff --git a/src/screens/PostThread/index.tsx b/src/screens/PostThread/index.tsx index b743d83436..46e1d65757 100644 --- a/src/screens/PostThread/index.tsx +++ b/src/screens/PostThread/index.tsx @@ -1,11 +1,10 @@ -import {useMemo, useRef, useState} from 'react' +import {useCallback, useMemo, useRef, useState} from 'react' import {useWindowDimensions, View} from 'react-native' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' +import {Trans} from '@lingui/macro' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' -import {cleanError} from '#/lib/strings/errors' +import {type ThreadViewOption} from '#/state/queries/preferences/useThreadPreferences' import {type ThreadItem, usePostThread} from '#/state/queries/usePostThread' import {type OnPostSuccessData} from '#/state/shell/composer' import {PostThreadComposePrompt} from '#/view/com/post-thread/PostThreadComposePrompt' @@ -15,6 +14,7 @@ import { ThreadAnchor, ThreadAnchorSkeleton, } from '#/screens/PostThread/components/ThreadAnchor' +import {ThreadError} from '#/screens/PostThread/components/ThreadError' import { ThreadItemPost, ThreadItemPostSkeleton, @@ -46,17 +46,19 @@ export function Inner({uri}: {uri: string | undefined}) { */ const thread = usePostThread({anchor: uri}) - const optimisticOnPostReply = (payload: OnPostSuccessData) => { - if (payload) { - const {replyToUri, posts} = payload - if (replyToUri && posts.length) { - thread.actions.insertReplies(replyToUri, posts) - } - } - } - const {openComposer} = useOpenComposer() - const onReplyToAnchor = () => { + const optimisticOnPostReply = useCallback( + (payload: OnPostSuccessData) => { + if (payload) { + const {replyToUri, posts} = payload + if (replyToUri && posts.length) { + thread.actions.insertReplies(replyToUri, posts) + } + } + }, + [thread], + ) + const onReplyToAnchor = useCallback(() => { const anchorPost = thread.data.items.find( slice => slice.type === 'threadPost' && slice.ui.isAnchor, ) @@ -75,7 +77,7 @@ export function Inner({uri}: {uri: string | undefined}) { }, onPostSuccess: optimisticOnPostReply, }) - } + }, [thread, openComposer, optimisticOnPostReply]) const [maxParentCount, setMaxParentCount] = useState(PARENT_CHUNK_SIZE) const [maxChildrenCount, setMaxChildrenCount] = useState(CHILDREN_CHUNK_SIZE) @@ -239,105 +241,125 @@ export function Inner({uri}: {uri: string | undefined}) { ) }, [slices]) - const renderItem = ({item, index}: {item: ThreadItem; index: number}) => { - if (item.type === 'threadPost') { - if (item.depth < 0) { - if (deferParents) return null - return ( - - ) - } else if (item.depth === 0) { - return ( - setDeferParents(false)}> - - - ) - } else { - if (thread.state.view === 'tree') { - return ( - 0, - }} - onPostSuccess={optimisticOnPostReply} - /> - ) - } else { + const renderItem = useCallback( + ({item, index}: {item: ThreadItem; index: number}) => { + if (item.type === 'threadPost') { + if (item.depth < 0) { return ( 0, + topBorder: index === 0, }} onPostSuccess={optimisticOnPostReply} /> ) - } - } - } else if (item.type === 'readMore') { - return ( - - ) - } else if (item.type === 'readMoreUp') { - return - } else if (item.type === 'threadPostBlocked') { - return - } else if (item.type === 'threadPostNotFound') { - return - } else if (item.type === 'replyComposer') { - return ( - - {gtPhone && ( - - )} - - ) - } else if (item.type === 'showOtherReplies') { - return - } else if (item.type === 'skeleton') { - if (item.item === 'anchor') { - return - } else if (item.item === 'reply') { - if (thread.state.view === 'linear') { - return + } else if (item.depth === 0) { + return ( + setDeferParents(false)}> + + + ) } else { - return + if (thread.state.view === 'tree') { + return ( + 0, + }} + onPostSuccess={optimisticOnPostReply} + /> + ) + } else { + return ( + 0, + }} + onPostSuccess={optimisticOnPostReply} + /> + ) + } + } + } else if (item.type === 'readMore') { + return ( + + ) + } else if (item.type === 'readMoreUp') { + return + } else if (item.type === 'threadPostBlocked') { + return + } else if (item.type === 'threadPostNotFound') { + return + } else if (item.type === 'replyComposer') { + return ( + + {gtPhone && ( + + )} + + ) + } else if (item.type === 'showOtherReplies') { + return + } else if (item.type === 'skeleton') { + if (item.item === 'anchor') { + return + } else if (item.item === 'reply') { + if (thread.state.view === 'linear') { + return + } else { + return + } + } else if (item.item === 'replyComposer') { + return } - } else if (item.item === 'replyComposer') { - return } - } - return null - } + return null + }, + [thread, optimisticOnPostReply, onReplyToAnchor, gtPhone], + ) + + const setSortWrapped = useCallback( + (sort: string) => { + setDeferParents(true) + shouldScrollToAnchor.current = true + thread.actions.setSort(sort) + }, + [thread, setDeferParents], + ) + + const setViewWrapped = useCallback( + (view: ThreadViewOption) => { + thread.actions.setView(view) + setDeferParents(true) + shouldScrollToAnchor.current = true + }, + [thread, setDeferParents], + ) return ( <> @@ -351,23 +373,18 @@ export function Inner({uri}: {uri: string | undefined}) { { - thread.actions.setSort(val) - setDeferParents(true) - shouldScrollToAnchor.current = true - }} + setSort={setSortWrapped} view={thread.state.view} - setView={val => { - thread.actions.setView(val) - setDeferParents(true) - shouldScrollToAnchor.current = true - }} + setView={setViewWrapped} /> {thread.state.error ? ( - + ) : ( { - 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 -} - const keyExtractor = (item: ThreadItem) => { return item.key }