Show a reply count on home network notes and stop virtualizing the feed

This commit is contained in:
Coracle-Bot 2026-09-09 15:09:45 +00:00 committed by hodlbod
parent 9027634567
commit b79025f827
5 changed files with 58 additions and 47 deletions

View file

@ -1571,6 +1571,9 @@ Acceptance:
- The Network section lists notes from her follows, resolved through the relays
those people publish to.
- It is a list of notes: a reply is counted on the note it answers rather than
drawn underneath it, and never appears as an item of its own.
- Every note carries its reply count, including the ones with no replies.
- Scrolling to the end of the feed loads more rather than asking her to.
### US-106 — Share text into the app

View file

@ -417,6 +417,8 @@ test("US-116 read the home dashboard", async ({seed, as}) => {
test("US-117 read the network feed on home", async ({seed, as}) => {
const note = "the tide charts are wrong again"
const quiet = "the ferry is running on time"
const reply = "they were reprinted last week"
await seed(({relay, user}) => {
const space = relay("space")
@ -433,13 +435,31 @@ test("US-117 read the network feed on home", async ({seed, as}) => {
space.event(user.alice, () =>
space.kind(FollowList).writer().follow(user.bob.pubkey).renderTemplate(),
)
space.event(user.bob, () => space.kind(Note).writer().setContent(note).renderTemplate())
const posted = space.event(user.bob, () =>
space.kind(Note).writer().setContent(note).renderTemplate(),
)
space.event(user.bob, () => space.kind(Note).writer().setContent(quiet).renderTemplate())
space.event(user.bob, () =>
space.kind(Note).writer().setParent(posted.event).setContent(reply).renderTemplate(),
)
})
const page = await as(users.alice, "/home")
await expect(page.getByRole("heading", {name: "Network"})).toBeVisible()
await expect(page.getByText(note)).toBeVisible()
// The feed is notes only: a reply is counted on the note it answers rather than drawn
// underneath it, and it never gets a card of its own.
await expect(page.getByRole("button", {name: "1 reply", exact: true})).toBeVisible()
await expect(page.getByText(reply)).toHaveCount(0)
// The count is there whether or not anybody replied, so every note reads the same.
await expect(page.getByText(quiet)).toBeVisible()
await expect(page.getByRole("button", {name: "0 replies"})).toBeVisible()
})
test("US-106 share text into the app", async ({seed, as}) => {

View file

@ -24,10 +24,9 @@
setReplyTo?: (comment?: TrustedEvent) => void
url?: string
context: FeedContext
maxDepth?: number
}
const {node, root, replyTo, setReplyTo, url, context, maxDepth = Infinity}: Props = $props()
const {node, root, replyTo, setReplyTo, url, context}: Props = $props()
const composing = $derived(replyTo?.id === node.comment.id)
@ -94,21 +93,14 @@
onSubmit={clearReplyTo} />
</div>
{/if}
{#if node.children.length > 0 && maxDepth > 1}
{#if node.children.length > 0}
<!-- The thread line runs under the avatar's center and indents replies to line up with
this comment's text column; it brightens while the subtree is hovered. -->
<div
data-component="CommentReplies"
class="border-line-less hover:border-line ml-4 flex flex-col border-l pl-7 transition-colors">
{#each node.children as child (child.comment.id)}
<CommentTree
node={child}
{root}
{replyTo}
{setReplyTo}
{url}
{context}
maxDepth={maxDepth - 1} />
<CommentTree node={child} {root} {replyTo} {setReplyTo} {url} {context} />
{/each}
</div>
{/if}

View file

@ -1,12 +1,12 @@
<script lang="ts">
import {sortBy} from "@welshman/lib"
import {COMMENT, NOTE} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import Cv from "@lib/components/Cv.svelte"
import Reply from "@assets/icons/reply-2.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import NoteItem from "@app/components/NoteItem.svelte"
import CommentTree from "@app/components/CommentTree.svelte"
import type {FeedContext} from "@app/feeds"
import {buildCommentTree} from "@app/social"
import {goToEvent} from "@app/routes"
type Props = {
event: TrustedEvent
@ -17,26 +17,18 @@
const related = context.related(event)
// buildCommentTree adopts a comment whose parent never loaded, and reads oldest first so
// that it adopts a parent before its own children.
const nodes = $derived(
buildCommentTree(
event,
sortBy(
e => e.created_at,
$related.filter(e => e.kind === COMMENT || e.kind === NOTE),
),
),
)
// Kind 1 notes are replied to with notes as well as with NIP-22 comments.
const replyCount = $derived($related.filter(e => e.kind === COMMENT || e.kind === NOTE).length)
const goToReplies = () => goToEvent(event)
</script>
<Cv class="card card-interactive flex flex-col gap-3">
<NoteItem {event} {context} class="" />
{#if nodes.length > 0}
<div class="border-line-less flex flex-col border-t">
{#each nodes as node (node.comment.id)}
<CommentTree {node} root={event} {context} maxDepth={2} />
{/each}
</div>
{/if}
</Cv>
<div class="card card-interactive flex flex-col gap-3">
<NoteItem {event} {context} card={false}>
<Button class="button button-neutral button-xs rounded-full" onclick={goToReplies}>
<Icon icon={Reply} size={4} />
{replyCount}
{replyCount === 1 ? "reply" : "replies"}
</Button>
</NoteItem>
</div>

View file

@ -19,16 +19,10 @@
children?: Snippet
context: FeedContext
url?: string
class?: string
card?: boolean
}
const {
url,
event,
children,
context,
class: className = "card card-interactive",
}: Props = $props()
const {url, event, children, context, card = true}: Props = $props()
const getRelays = () => (url ? [url] : $router.resolver.relays([seen(event)]))
@ -41,7 +35,7 @@
const onEmoji = (emoji: NativeEmoji) => createReaction({content: emoji.unicode, tags: []})
</script>
<Cv tag={NoteCard} {event} {url} class={className}>
{#snippet body()}
<NoteContent {event} expandMode="inline" />
<div class="flex w-full justify-between gap-2">
<ReactionSummary
@ -60,4 +54,14 @@
</ReactionSummary>
{@render children?.()}
</div>
</Cv>
{/snippet}
{#if card}
<Cv tag={NoteCard} {event} {url} class="card card-interactive">
{@render body()}
</Cv>
{:else}
<NoteCard {event} {url}>
{@render body()}
</NoteCard>
{/if}