diff --git a/assets/icons/circlePlus_stroke2_corner0_rounded.svg b/assets/icons/circlePlus_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..1feeb3c3ed --- /dev/null +++ b/assets/icons/circlePlus_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/src/components/icons/CirclePlus.tsx b/src/components/icons/CirclePlus.tsx new file mode 100644 index 0000000000..690e77326e --- /dev/null +++ b/src/components/icons/CirclePlus.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const CirclePlus_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-16Zm0 3a1 1 0 0 1 1 1v3h3l.102.005a1 1 0 0 1 0 1.99L16 13h-3v3a1 1 0 1 1-2 0v-3H8a1 1 0 0 1 0-2h3V8a1 1 0 0 1 1-1Z', +}) diff --git a/src/state/queries/post-thread.ts b/src/state/queries/post-thread.ts index 4a75f85a79..7415283dc3 100644 --- a/src/state/queries/post-thread.ts +++ b/src/state/queries/post-thread.ts @@ -47,7 +47,8 @@ export interface ThreadCtx { isHighlightedPost?: boolean hasMore?: boolean /** - * Means the loading state has parents + * Means the loading state has parents, but once the data loads we don't even + * populate this value, so it's the same as `threadNode.parents.length` */ isParentLoading?: boolean /** diff --git a/src/state/queries/usePostThread/index.ts b/src/state/queries/usePostThread/index.ts index d0a265398c..f45d60bf6c 100644 --- a/src/state/queries/usePostThread/index.ts +++ b/src/state/queries/usePostThread/index.ts @@ -41,11 +41,12 @@ export function usePostThread({ const query = useQuery({ enabled, queryKey, + gcTime: 0, async queryFn() { const {data} = await agent.app.bsky.unspecced.getPostThreadV2({ anchor: uri!, - branchingFactor: params.view === 'linear' ? 1 : 100, - below: 10, + branchingFactor: params.view === 'linear' ? 1 : 3, // 100 TODO + below: 3, sorting: mapSortOptionsToSortID(params.sort), }) return data @@ -95,6 +96,7 @@ export function usePostThread({ return { ...query, data: { + anchorIndex: items.findIndex(i => Boolean(i.ui?.isAnchor)), items, threadgate: query.data?.threadgate, }, diff --git a/src/state/queries/usePostThread/traversal.ts b/src/state/queries/usePostThread/traversal.ts index 4da1935ba1..fa2a138386 100644 --- a/src/state/queries/usePostThread/traversal.ts +++ b/src/state/queries/usePostThread/traversal.ts @@ -1,4 +1,6 @@ import { + APP_BSKY_UNSPECCED, + AtUri, AppBskyUnspeccedGetPostThreadV2, type ModerationDecision, type ModerationOpts, @@ -21,6 +23,54 @@ export function flatten( ) { const flattened: Slice[] = sorted.items + const unhydratedReplyIntervals = [] + + for (let i = 0; i < flattened.length; i++) { + const item = flattened[i] + + if (item.type === 'threadPost') { + // TODO should not insert if not found post etc + if (item.ui.isAnchor && hasSession && !item.value.post.viewer?.replyDisabled) { + flattened.splice(i + 1, 0, { + type: 'replyComposer', + key: 'replyComposer', + }) + } + + const prev = unhydratedReplyIntervals[unhydratedReplyIntervals.length - 1] + + if (item.annotations.has(APP_BSKY_UNSPECCED.GetPostThreadV2HasMoreReplies)) { + unhydratedReplyIntervals.push({ + item, + replyCount: item.value.post.replyCount || 0, + }) + } + + /* + * If direct child of previous item with `hasMoreReplies`, subtract + */ + if (prev && item.depth === prev.item.depth + 1) { + prev.replyCount = Math.max(0, prev.replyCount - 1) + } + + if (prev && item.depth <= prev.item.depth) { + flattened.splice(i, 0, { + type: 'readMore', + key: `readMore:${prev.item.uri}`, + indent: prev.item.depth + (item.depth < prev.item.depth ? -1 : 0), + replyCount: prev.replyCount, + nextAnchor: prev.item, + nextAnchorUri: new AtUri(prev.item.uri), + }) + unhydratedReplyIntervals.pop() + } + } + } + + /* + * Insert hidden items and buttons to show them + */ + if (sorted.hidden.length) { if (showHidden) { flattened.push(...sorted.hidden) @@ -55,30 +105,6 @@ export function flatten( } } - if (hasSession) { - for (let i = 0; i < flattened.length; i++) { - const item = flattened[i] - - // TODO should not insert if not found post etc - if (item.type === 'threadPost') { - if (item.ui.isAnchor) { - flattened.splice(i + 1, 0, { - type: 'replyComposer', - key: 'replyComposer', - }) - } - - if ( - item.value.post.replyCount && - item.value.post.replyCount > 0 && - !item.ui.showChildReplyLine - ) { - console.log('insert more link') - } - } - } - } - return flattened } diff --git a/src/state/queries/usePostThread/types.ts b/src/state/queries/usePostThread/types.ts index d3adffeb9b..4a7f36ff20 100644 --- a/src/state/queries/usePostThread/types.ts +++ b/src/state/queries/usePostThread/types.ts @@ -1,6 +1,7 @@ import { // eslint-disable-next-line @typescript-eslint/no-unused-vars type APP_BSKY_UNSPECCED, + type AtUri, type AppBskyFeedDefs, type AppBskyFeedPost, type AppBskyUnspeccedGetPostThreadV2, @@ -89,7 +90,10 @@ export type Slice = kind: HiddenReplyKind } | { - type: 'threadPostNoOp' + type: 'readMore' key: string - comment: string + indent: number + replyCount: number + nextAnchor: Extract + nextAnchorUri: AtUri } diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index 69c72ebb38..1d22893340 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -90,7 +90,6 @@ type ThreadSkeletonParts = { } const keyExtractor = (item: RowItem) => { - console.log(item._reactKey) return item._reactKey } @@ -258,7 +257,6 @@ export function PostThread({uri}: {uri: string | undefined}) { fetchedAt, randomCache, ]) - console.log({thread, skeleton}) const error = React.useMemo(() => { if (AppBskyFeedDefs.isNotFoundPost(thread)) { @@ -299,6 +297,9 @@ export function PostThread({uri}: {uri: string | undefined}) { // maintainVisibleContentPosition and onContentSizeChange // to "hold onto" the correct row instead of the first one. + /* + * This is basically `!!parents.length`, see notes on `isParentLoading` + */ if (!highlightedPost.ctx.isParentLoading && !deferParents) { // When progressively revealing parents, rendering a placeholder // here will cause scrolling jumps. Don't add it unless you test it. @@ -325,6 +326,8 @@ export function PostThread({uri}: {uri: string | undefined}) { return arr }, [skeleton, deferParents, maxParents, maxReplies]) + console.log({anchorIndex: posts.findIndex(p => p.ctx?.isHighlightedPost)}) + // 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(false) @@ -340,11 +343,8 @@ export function PostThread({uri}: {uri: string | undefined}) { const headerNode = headerRef.current if (postNode && headerNode) { let pageY = (postNode as any as Element).getBoundingClientRect().top - console.log({pageY}) pageY -= (headerNode as any as Element).getBoundingClientRect().height - console.log({pageY}) pageY = Math.max(0, pageY) - console.log({pageY}) ref.current?.scrollToOffset({ animated: false, offset: pageY, @@ -423,6 +423,8 @@ export function PostThread({uri}: {uri: string | undefined}) { (skeleton.highlightedPost.ctx.isParentLoading || Boolean(skeleton?.parents && skeleton.parents.length > 0)) + console.log({hasParents}) + const renderItem = ({item, index}: {item: RowItem; index: number}) => { if (item === REPLY_PROMPT && hasSession) { return ( diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx index 7db047b7af..68001a3ce3 100644 --- a/src/view/screens/PostThread.tsx +++ b/src/view/screens/PostThread.tsx @@ -13,9 +13,7 @@ import {cleanError} from '#/lib/strings/errors' import {makeRecordUri} from '#/lib/strings/url-helpers' import {isNative} from '#/platform/detection' import {useSetMinimalShellMode} from '#/state/shell' -{ - /* import {PostThread as PostThreadComponent} from '#/view/com/post-thread/PostThread' */ -} +import {PostThread as PostThreadComponent} from '#/view/com/post-thread/PostThread' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {ScrollProvider} from '#/lib/ScrollContext' @@ -32,7 +30,10 @@ import {PostThreadShowHiddenReplies} from '#/view/com/post-thread/PostThreadShow import {List, type ListMethods} from '#/view/com/util/List' import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' +import {Link} from '#/components/Link' +import {makeProfileLink} from '#/lib/routes/links' import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from '#/components/icons/SettingsSlider' +import {CirclePlus_Stroke2_Corner0_Rounded as CirclePlus} from '#/components/icons/CirclePlus' import * as Layout from '#/components/Layout' import {ListFooter} from '#/components/Lists' import * as Menu from '#/components/Menu' @@ -41,7 +42,7 @@ import {Text} from '#/components/Typography' 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: 1, + minIndexForVisible: 0, } type Props = NativeStackScreenProps @@ -232,13 +233,11 @@ export function Inner({uri}: {uri: string | undefined}) { // prevPost={prev} // nextPost={next} isHighlightedPost={item.ui.isAnchor} - // @ts-expect-error - hasMore={item.value.hasUnhydratedReplies} + hasMore={false} // TODO need to replace this entirely showChildReplyLine={item.ui.showChildReplyLine} showParentReplyLine={item.ui.showParentReplyLine} hasPrecedingItem={ - // @ts-expect-error - item.ui.showParentReplyLine || !!item.value.hasUnhydratedParents + item.ui.showParentReplyLine } // !!hasUnrevealedParents // TODO overrideBlur={ shownHiddenReplyKinds.has(HiddenReplyKind.Muted) && item.depth > 0 @@ -248,6 +247,68 @@ export function Inner({uri}: {uri: string | undefined}) { /> ) + } else if (item.type === 'readMore') { + return ( + + {Array.from(Array(item.indent - 1)).map((_, n: number) => ( + + ))} + + + + + {({hovered, pressed}) => { + return ( + <> + + + Read {item.replyCount} more replies + + + ) + }} + + + ) } else if (item.type === 'threadPostBlocked') { return ( @@ -337,7 +400,7 @@ export function Inner({uri}: {uri: string | undefined}) { */ maintainVisibleContentPosition={ isNative // && hasParents // TODO not sure we need this - ? MAINTAIN_VISIBLE_CONTENT_POSITION + ? { minIndexForVisible: 0 } // MAINTAIN_VISIBLE_CONTENT_POSITION : undefined } desktopFixedHeight