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
|
- The Network section lists notes from her follows, resolved through the relays
|
||||||
those people publish to.
|
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.
|
- Scrolling to the end of the feed loads more rather than asking her to.
|
||||||
|
|
||||||
### US-106 — Share text into the app
|
### 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}) => {
|
test("US-117 read the network feed on home", async ({seed, as}) => {
|
||||||
const note = "the tide charts are wrong again"
|
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}) => {
|
await seed(({relay, user}) => {
|
||||||
const space = relay("space")
|
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.event(user.alice, () =>
|
||||||
space.kind(FollowList).writer().follow(user.bob.pubkey).renderTemplate(),
|
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")
|
const page = await as(users.alice, "/home")
|
||||||
|
|
||||||
await expect(page.getByRole("heading", {name: "Network"})).toBeVisible()
|
await expect(page.getByRole("heading", {name: "Network"})).toBeVisible()
|
||||||
await expect(page.getByText(note)).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}) => {
|
test("US-106 share text into the app", async ({seed, as}) => {
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,9 @@
|
||||||
setReplyTo?: (comment?: TrustedEvent) => void
|
setReplyTo?: (comment?: TrustedEvent) => void
|
||||||
url?: string
|
url?: string
|
||||||
context: FeedContext
|
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)
|
const composing = $derived(replyTo?.id === node.comment.id)
|
||||||
|
|
||||||
|
|
@ -94,21 +93,14 @@
|
||||||
onSubmit={clearReplyTo} />
|
onSubmit={clearReplyTo} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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
|
<!-- 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. -->
|
this comment's text column; it brightens while the subtree is hovered. -->
|
||||||
<div
|
<div
|
||||||
data-component="CommentReplies"
|
data-component="CommentReplies"
|
||||||
class="border-line-less hover:border-line ml-4 flex flex-col border-l pl-7 transition-colors">
|
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)}
|
{#each node.children as child (child.comment.id)}
|
||||||
<CommentTree
|
<CommentTree node={child} {root} {replyTo} {setReplyTo} {url} {context} />
|
||||||
node={child}
|
|
||||||
{root}
|
|
||||||
{replyTo}
|
|
||||||
{setReplyTo}
|
|
||||||
{url}
|
|
||||||
{context}
|
|
||||||
maxDepth={maxDepth - 1} />
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {sortBy} from "@welshman/lib"
|
|
||||||
import {COMMENT, NOTE} from "@welshman/util"
|
import {COMMENT, NOTE} from "@welshman/util"
|
||||||
import type {TrustedEvent} 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 NoteItem from "@app/components/NoteItem.svelte"
|
||||||
import CommentTree from "@app/components/CommentTree.svelte"
|
|
||||||
import type {FeedContext} from "@app/feeds"
|
import type {FeedContext} from "@app/feeds"
|
||||||
import {buildCommentTree} from "@app/social"
|
import {goToEvent} from "@app/routes"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
|
|
@ -17,26 +17,18 @@
|
||||||
|
|
||||||
const related = context.related(event)
|
const related = context.related(event)
|
||||||
|
|
||||||
// buildCommentTree adopts a comment whose parent never loaded, and reads oldest first so
|
// Kind 1 notes are replied to with notes as well as with NIP-22 comments.
|
||||||
// that it adopts a parent before its own children.
|
const replyCount = $derived($related.filter(e => e.kind === COMMENT || e.kind === NOTE).length)
|
||||||
const nodes = $derived(
|
|
||||||
buildCommentTree(
|
const goToReplies = () => goToEvent(event)
|
||||||
event,
|
|
||||||
sortBy(
|
|
||||||
e => e.created_at,
|
|
||||||
$related.filter(e => e.kind === COMMENT || e.kind === NOTE),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Cv class="card card-interactive flex flex-col gap-3">
|
<div class="card card-interactive flex flex-col gap-3">
|
||||||
<NoteItem {event} {context} class="" />
|
<NoteItem {event} {context} card={false}>
|
||||||
{#if nodes.length > 0}
|
<Button class="button button-neutral button-xs rounded-full" onclick={goToReplies}>
|
||||||
<div class="border-line-less flex flex-col border-t">
|
<Icon icon={Reply} size={4} />
|
||||||
{#each nodes as node (node.comment.id)}
|
{replyCount}
|
||||||
<CommentTree {node} root={event} {context} maxDepth={2} />
|
{replyCount === 1 ? "reply" : "replies"}
|
||||||
{/each}
|
</Button>
|
||||||
</div>
|
</NoteItem>
|
||||||
{/if}
|
</div>
|
||||||
</Cv>
|
|
||||||
|
|
|
||||||
|
|
@ -19,16 +19,10 @@
|
||||||
children?: Snippet
|
children?: Snippet
|
||||||
context: FeedContext
|
context: FeedContext
|
||||||
url?: string
|
url?: string
|
||||||
class?: string
|
card?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {url, event, children, context, card = true}: Props = $props()
|
||||||
url,
|
|
||||||
event,
|
|
||||||
children,
|
|
||||||
context,
|
|
||||||
class: className = "card card-interactive",
|
|
||||||
}: Props = $props()
|
|
||||||
|
|
||||||
const getRelays = () => (url ? [url] : $router.resolver.relays([seen(event)]))
|
const getRelays = () => (url ? [url] : $router.resolver.relays([seen(event)]))
|
||||||
|
|
||||||
|
|
@ -41,7 +35,7 @@
|
||||||
const onEmoji = (emoji: NativeEmoji) => createReaction({content: emoji.unicode, tags: []})
|
const onEmoji = (emoji: NativeEmoji) => createReaction({content: emoji.unicode, tags: []})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Cv tag={NoteCard} {event} {url} class={className}>
|
{#snippet body()}
|
||||||
<NoteContent {event} expandMode="inline" />
|
<NoteContent {event} expandMode="inline" />
|
||||||
<div class="flex w-full justify-between gap-2">
|
<div class="flex w-full justify-between gap-2">
|
||||||
<ReactionSummary
|
<ReactionSummary
|
||||||
|
|
@ -60,4 +54,14 @@
|
||||||
</ReactionSummary>
|
</ReactionSummary>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</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