Show a reply count on home network notes and stop virtualizing the feed
This commit is contained in:
parent
9027634567
commit
b79025f827
5 changed files with 58 additions and 47 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}) => {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Reference in a new issue