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
}