Show a timestamp on every note card and widen the home network feed to all content kinds
This commit is contained in:
parent
b79025f827
commit
3e265a8a39
3 changed files with 41 additions and 19 deletions
|
|
@ -419,6 +419,7 @@ 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 quiet = "the ferry is running on time"
|
||||||
const reply = "they were reprinted last week"
|
const reply = "they were reprinted last week"
|
||||||
|
const topic = "Dredging the channel"
|
||||||
|
|
||||||
await seed(({relay, user}) => {
|
await seed(({relay, user}) => {
|
||||||
const space = relay("space")
|
const space = relay("space")
|
||||||
|
|
@ -445,6 +446,16 @@ test("US-117 read the network feed on home", async ({seed, as}) => {
|
||||||
space.event(user.bob, () =>
|
space.event(user.bob, () =>
|
||||||
space.kind(Note).writer().setParent(posted.event).setContent(reply).renderTemplate(),
|
space.kind(Note).writer().setParent(posted.event).setContent(reply).renderTemplate(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
space.event(user.bob, () =>
|
||||||
|
space
|
||||||
|
.kind(Thread)
|
||||||
|
.writer()
|
||||||
|
.setRoom(space.url, "general")
|
||||||
|
.setTitle(topic)
|
||||||
|
.setContent("The barges keep grounding.")
|
||||||
|
.renderTemplate(),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const page = await as(users.alice, "/home")
|
const page = await as(users.alice, "/home")
|
||||||
|
|
@ -460,6 +471,16 @@ test("US-117 read the network feed on home", async ({seed, as}) => {
|
||||||
// The count is there whether or not anybody replied, so every note reads the same.
|
// The count is there whether or not anybody replied, so every note reads the same.
|
||||||
await expect(page.getByText(quiet)).toBeVisible()
|
await expect(page.getByText(quiet)).toBeVisible()
|
||||||
await expect(page.getByRole("button", {name: "0 replies"})).toBeVisible()
|
await expect(page.getByRole("button", {name: "0 replies"})).toBeVisible()
|
||||||
|
|
||||||
|
// The feed carries every kind of post a follow writes, not only their notes.
|
||||||
|
await expect(page.getByText(topic)).toBeVisible()
|
||||||
|
|
||||||
|
// Every card says when it was posted.
|
||||||
|
const network = page
|
||||||
|
.locator("section")
|
||||||
|
.filter({has: page.getByRole("heading", {name: "Network"})})
|
||||||
|
|
||||||
|
await expect(network.getByRole("button", {name: /\d+\/\d+\/\d+/}).first()).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("US-106 share text into the app", async ({seed, as}) => {
|
test("US-106 share text into the app", async ({seed, as}) => {
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
import {onDestroy} from "svelte"
|
import {onDestroy} from "svelte"
|
||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import type {Writable} from "svelte/store"
|
import type {Writable} from "svelte/store"
|
||||||
import {sortBy} from "@welshman/lib"
|
import {sortBy, uniqBy} from "@welshman/lib"
|
||||||
import type {Maybe} from "@welshman/lib"
|
import type {Maybe} from "@welshman/lib"
|
||||||
import {NOTE, outbox} from "@welshman/util"
|
import {NOTE, getIdOrAddress, outbox} from "@welshman/util"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {getReplyTags} from "@welshman/domain"
|
import {getReplyTags} from "@welshman/domain"
|
||||||
import Planet from "@assets/icons/planet.svg?dataurl"
|
import Planet from "@assets/icons/planet.svg?dataurl"
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import HomeSection from "@app/components/HomeSection.svelte"
|
import HomeSection from "@app/components/HomeSection.svelte"
|
||||||
import HomeNetworkItem from "@app/components/HomeNetworkItem.svelte"
|
import HomeNetworkItem from "@app/components/HomeNetworkItem.svelte"
|
||||||
|
import {CONTENT_KINDS} from "@app/content"
|
||||||
import {followLists, relayLists, router, user} from "@app/core"
|
import {followLists, relayLists, router, user} from "@app/core"
|
||||||
import {isFeedLoading, makeFeed, makeFeedContext, makeScrollLoader} from "@app/feeds"
|
import {isFeedLoading, makeFeed, makeFeedContext, makeScrollLoader} from "@app/feeds"
|
||||||
|
|
||||||
|
|
@ -36,23 +37,27 @@
|
||||||
let started = false
|
let started = false
|
||||||
let stop: Maybe<() => void>
|
let stop: Maybe<() => void>
|
||||||
|
|
||||||
// Replies are left out - without their parent they read as half a conversation.
|
// Kind 1 replies are left out - without their parent they read as half a conversation. Every
|
||||||
const notes = $derived(
|
// other kind here is a root, and an edited one is shown once, at its latest version.
|
||||||
sortBy(
|
const isRoot = (event: TrustedEvent) =>
|
||||||
e => -e.created_at,
|
event.kind !== NOTE || getReplyTags(event.tags).replies.length === 0
|
||||||
$events.filter(e => getReplyTags(e.tags).replies.length === 0),
|
|
||||||
|
const items = $derived(
|
||||||
|
uniqBy(
|
||||||
|
getIdOrAddress,
|
||||||
|
sortBy(e => -e.created_at, $events.filter(isRoot)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const loading = $derived(isFeedLoading($older))
|
const loading = $derived(isFeedLoading($older))
|
||||||
const exhausted = $derived($older?.status === "exhausted")
|
const exhausted = $derived($older?.status === "exhausted")
|
||||||
const isEmpty = $derived(follows?.length === 0 || (exhausted && notes.length === 0))
|
const isEmpty = $derived(follows?.length === 0 || (exhausted && items.length === 0))
|
||||||
|
|
||||||
const start = async (pubkeys: string[]) => {
|
const start = async (pubkeys: string[]) => {
|
||||||
const scenario = await $router.resolve(pubkeys.map(pubkey => outbox(pubkey)))
|
const scenario = await $router.resolve(pubkeys.map(pubkey => outbox(pubkey)))
|
||||||
const feed = makeFeed({
|
const feed = makeFeed({
|
||||||
relays: scenario.limit(RELAY_LIMIT).getUrls(),
|
relays: scenario.limit(RELAY_LIMIT).getUrls(),
|
||||||
filters: [{kinds: [NOTE], authors: pubkeys}],
|
filters: [{kinds: [NOTE, ...CONTENT_KINDS], authors: pubkeys}],
|
||||||
onEvent: context.add,
|
onEvent: context.add,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -85,16 +90,16 @@
|
||||||
<div class="flex flex-col items-center gap-3 pb-4 text-center">
|
<div class="flex flex-col items-center gap-3 pb-4 text-center">
|
||||||
<p class="font-medium">Follow a few people to fill this out</p>
|
<p class="font-medium">Follow a few people to fill this out</p>
|
||||||
<p class="max-w-md text-sm opacity-75">
|
<p class="max-w-md text-sm opacity-75">
|
||||||
Notes from the people you follow collect here. Spaces are a good place to find some.
|
Posts from the people you follow collect here. Spaces are a good place to find some.
|
||||||
</p>
|
</p>
|
||||||
<Link href="/spaces" class="button button-neutral button-sm">Browse spaces</Link>
|
<Link href="/spaces" class="button button-neutral button-sm">Browse spaces</Link>
|
||||||
</div>
|
</div>
|
||||||
{:else if notes.length === 0}
|
{:else if items.length === 0}
|
||||||
<div class="flex justify-center pb-4">
|
<div class="flex justify-center pb-4">
|
||||||
<Spinner loading>Looking for notes from people you follow…</Spinner>
|
<Spinner loading>Looking for posts from people you follow…</Spinner>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<Masonry items={notes} getKey={event => event.id} columnWidth={80} maxColumns={2} gap={3}>
|
<Masonry {items} getKey={event => event.id} columnWidth={80} maxColumns={2} gap={3}>
|
||||||
{#snippet child(event)}
|
{#snippet child(event)}
|
||||||
<HomeNetworkItem {event} {context} />
|
<HomeNetworkItem {event} {context} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,7 @@
|
||||||
<Button class="link ml-8" onclick={ignoreMute}>Show anyway</Button>
|
<Button class="link ml-8" onclick={ignoreMute}>Show anyway</Button>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- The size container is the header, not the card: container-type contains a box's inline
|
<div class="flex items-start justify-between gap-2">
|
||||||
size, so a card that is its own container reports no width to an ancestor sized by its
|
|
||||||
contents and collapses a quote inside a chat bubble. The header fills the card either way,
|
|
||||||
so the query still measures the card's width. -->
|
|
||||||
<div class="@container flex items-start justify-between gap-2">
|
|
||||||
{#if !hideProfile}
|
{#if !hideProfile}
|
||||||
{#if minimal}
|
{#if minimal}
|
||||||
@<ProfileName pubkey={event.pubkey} {url} />
|
@<ProfileName pubkey={event.pubkey} {url} />
|
||||||
|
|
@ -55,7 +51,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
<Button
|
<Button
|
||||||
class={cx("text-sm opacity-75 @max-sm:hidden", {"text-xs": minimal})}
|
class={cx("whitespace-nowrap text-sm opacity-75", {"text-xs": minimal})}
|
||||||
onclick={goToNote}>
|
onclick={goToNote}>
|
||||||
{formatTimestamp(event.created_at)}
|
{formatTimestamp(event.created_at)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue