From 8144eac8a290c76b6d83d7842b984825c4423a03 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 5 Jun 2025 10:37:34 -0500 Subject: [PATCH] Add moreParents handling --- ...arrowTopCircle_stroke2_corner0_rounded.svg | 1 + src/components/icons/ArrowTopCircle.tsx | 5 ++ .../components/ThreadItemReadMoreUp.tsx | 90 +++++++++++++++++++ src/screens/PostThread/index.tsx | 7 +- src/state/queries/usePostThread/traversal.ts | 12 +++ src/state/queries/usePostThread/types.ts | 10 +++ src/state/queries/usePostThread/utils.ts | 8 +- src/state/queries/usePostThread/views.ts | 19 ++++ 8 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 assets/icons/arrowTopCircle_stroke2_corner0_rounded.svg create mode 100644 src/components/icons/ArrowTopCircle.tsx create mode 100644 src/screens/PostThread/components/ThreadItemReadMoreUp.tsx diff --git a/assets/icons/arrowTopCircle_stroke2_corner0_rounded.svg b/assets/icons/arrowTopCircle_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..e34b5eb38f --- /dev/null +++ b/assets/icons/arrowTopCircle_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/src/components/icons/ArrowTopCircle.tsx b/src/components/icons/ArrowTopCircle.tsx new file mode 100644 index 0000000000..2d250367f3 --- /dev/null +++ b/src/components/icons/ArrowTopCircle.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const ArrowTopCircle_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.63 3.225a1 1 0 0 1 1.337.068l3 3 .068.076a1 1 0 0 1-1.406 1.406l-.076-.068L13 10.414V16a1 1 0 1 1-2 0v-5.586l-1.293 1.293a1 1 0 1 1-1.414-1.414l3-3 .076-.068Z', +}) diff --git a/src/screens/PostThread/components/ThreadItemReadMoreUp.tsx b/src/screens/PostThread/components/ThreadItemReadMoreUp.tsx new file mode 100644 index 0000000000..eb8fcf2459 --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemReadMoreUp.tsx @@ -0,0 +1,90 @@ +import {memo} from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {type ThreadItem} from '#/state/queries/usePostThread' +import { + LINEAR_AVI_WIDTH, + OUTER_SPACE, + REPLY_LINE_WIDTH, +} from '#/screens/PostThread/const' +import {atoms as a, useTheme} from '#/alf' +import {ArrowTopCircle_Stroke2_Corner0_Rounded as UpIcon} from '#/components/icons/ArrowTopCircle' +import {Link} from '#/components/Link' +import {Text} from '#/components/Typography' + +export const ThreadItemReadMoreUp = memo(function ThreadItemReadMoreUp({ + item, +}: { + item: Extract +}) { + const t = useTheme() + const {_} = useLingui() + + return ( + + {({hovered, pressed}) => { + const interacted = hovered || pressed + return ( + + + + + + + Continue thread... + + + + + + + ) + }} + + ) +}) diff --git a/src/screens/PostThread/index.tsx b/src/screens/PostThread/index.tsx index c95f0c44b8..a4fcb31def 100644 --- a/src/screens/PostThread/index.tsx +++ b/src/screens/PostThread/index.tsx @@ -24,6 +24,7 @@ import { } from '#/screens/PostThread/components/ThreadItemPost' import {ThreadItemPostTombstone} from '#/screens/PostThread/components/ThreadItemPostTombstone' import {ThreadItemReadMore} from '#/screens/PostThread/components/ThreadItemReadMore' +import {ThreadItemReadMoreUp} from '#/screens/PostThread/components/ThreadItemReadMoreUp' import {ThreadItemTreePost} from '#/screens/PostThread/components/ThreadItemTreePost' import {useBreakpoints, web} from '#/alf' import * as Layout from '#/components/Layout' @@ -191,8 +192,8 @@ export function Inner({uri}: {uri: string | undefined}) { } } } else { - // ignore parents - if (hasDepth && item.depth < 0) continue + // ignore any parent items + if (item.type === 'readMoreUp' || (hasDepth && item.depth < 0)) continue // can exit early if we've reached the max children count if (childrenCount > maxChildrenCount) break @@ -271,6 +272,8 @@ export function Inner({uri}: {uri: string | undefined}) { view={thread.state.view === 'tree' ? 'tree' : 'linear'} /> ) + } else if (item.type === 'readMoreUp') { + return } else if (item.type === 'threadPostBlocked') { return } else if (item.type === 'threadPostNotFound') { diff --git a/src/state/queries/usePostThread/traversal.ts b/src/state/queries/usePostThread/traversal.ts index ff43fceaed..c987f9156f 100644 --- a/src/state/queries/usePostThread/traversal.ts +++ b/src/state/queries/usePostThread/traversal.ts @@ -353,6 +353,16 @@ export function sortAndAnnotateThreadItems( i++ } + /** + * Only occurs for the first item in the thread, which may have + * additional parents not included in this request. + */ + if (item.value.moreParents) { + metadata.followsReadMoreUp = true + subset.splice(i, 0, views.readMoreUp(metadata)) + i++ + } + /* * Calculate the final UI state for the thread item. */ @@ -362,6 +372,8 @@ export function sortAndAnnotateThreadItems( } } + console.log(threadItems) + return { threadItems, hiddenThreadItems, diff --git a/src/state/queries/usePostThread/types.ts b/src/state/queries/usePostThread/types.ts index d8f5a3dca4..8bfe3d96b5 100644 --- a/src/state/queries/usePostThread/types.ts +++ b/src/state/queries/usePostThread/types.ts @@ -100,6 +100,11 @@ export type ThreadItem = moreReplies: number skippedIndentIndices: Set } + | { + type: 'readMoreUp' + key: string + href: string + } | { type: 'skeleton' key: string @@ -117,6 +122,11 @@ export type TraversalMetadata = { * calculated on the server. */ depth: number + /** + * Indicates if this item is a "read more" link preceding this post that + * continues the thread upwards. + */ + followsReadMoreUp: boolean /** * Indicates if the post is the last reply beneath its parent post. */ diff --git a/src/state/queries/usePostThread/utils.ts b/src/state/queries/usePostThread/utils.ts index 3b2e6d8c14..f309be3e50 100644 --- a/src/state/queries/usePostThread/utils.ts +++ b/src/state/queries/usePostThread/utils.ts @@ -81,6 +81,10 @@ export function getTraversalMetadata({ * Unknown until after traversal */ precedesChildReadMore: false, + /* + * Unknown until after traversal + */ + followsReadMoreUp: false, postData: { uri: item.uri, authorHandle: item.value.post.author.handle, @@ -124,6 +128,7 @@ export function getThreadPostUI({ repliesSeenCount, repliesUnhydrated, precedesChildReadMore, + followsReadMoreUp, }: TraversalMetadata): Extract['ui'] { // TODO might be able to simplify this const isReplyAndHasReplies = @@ -134,7 +139,8 @@ export function getThreadPostUI({ return { isAnchor: depth === 0, showParentReplyLine: - !!prevItemDepth && prevItemDepth !== 0 && prevItemDepth < depth, + followsReadMoreUp || + (!!prevItemDepth && prevItemDepth !== 0 && prevItemDepth < depth), showChildReplyLine: depth < 0 || isReplyAndHasReplies, indent: depth, /* diff --git a/src/state/queries/usePostThread/views.ts b/src/state/queries/usePostThread/views.ts index a6f2f0f848..1028cd847e 100644 --- a/src/state/queries/usePostThread/views.ts +++ b/src/state/queries/usePostThread/views.ts @@ -124,6 +124,25 @@ export function readMore({ } } +export function readMoreUp({ + postData, +}: TraversalMetadata): Extract { + const urip = new AtUri(postData.uri) + const href = makeProfileLink( + { + did: urip.host, + handle: postData.authorHandle, + }, + 'post', + urip.rkey, + ) + return { + type: 'readMoreUp' as const, + key: `readMoreUp:${postData.uri}`, + href, + } +} + export function skeleton({ key, item,