Offer a space section when the relay holds one or something under it is unread
This commit is contained in:
parent
241d2b3737
commit
1881a3710f
4 changed files with 152 additions and 14 deletions
|
|
@ -1690,6 +1690,28 @@ Acceptance:
|
|||
- Opening the list keeps the dot on the corner of alice's card, and bob's own
|
||||
card carries none.
|
||||
|
||||
### US-123 — Find a section whose newest item is old
|
||||
|
||||
As bob, I want a space to offer every kind of content it holds, so that a quiet
|
||||
section is something I can reach rather than something I have to guess at.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- A poll older than the window the space sync asks for still puts the Polls nav
|
||||
item on the menu, and opening it lists the poll.
|
||||
|
||||
### US-124 — Reach a badge raised by content the space doesn't have
|
||||
|
||||
As bob, I want a badge on a space to lead me to whatever raised it, so that an
|
||||
indicator I can see is one I can clear.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- A comment on a poll the space doesn't hold raises a dot on the space, and the
|
||||
Polls nav item carries the same dot once the space is open.
|
||||
- Opening the list says there are no polls, and afterwards the space stops
|
||||
offering the section.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Features the e2e suite cannot exercise, and what stops it.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import {neventEncode, npubEncode} from "nostr-tools/nip19"
|
||||
import {HOUR, MINUTE} from "@welshman/lib"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import {DAY, HOUR, MINUTE} from "@welshman/lib"
|
||||
import {POLL, displayRelayUrl} from "@welshman/util"
|
||||
import {RelayMessageType} from "@welshman/net"
|
||||
import {Classified, FollowList, Note, Thread} from "@welshman/domain"
|
||||
import {Classified, Comment, FollowList, Note, Poll, Thread} from "@welshman/domain"
|
||||
import type {Locator, Page} from "@playwright/test"
|
||||
import {
|
||||
composer,
|
||||
|
|
@ -679,8 +679,8 @@ test("US-110 see another space's unread activity from a phone", async ({seed, as
|
|||
await expect(unreadDot(menuButton)).toHaveCount(0)
|
||||
})
|
||||
|
||||
// SpaceMenuNavItems hides a content type until the space has an event of that kind, so this link
|
||||
// appearing at all is what says the seeded threads loaded.
|
||||
// SpaceMenuNavItems offers a content type once the space has an event of that kind or something
|
||||
// under it is unread, so this link appearing at all is what says the seeded content loaded.
|
||||
const contentNavItem = (page: Page, name: string) =>
|
||||
page.locator(".secondary-nav").getByRole("link", {name})
|
||||
|
||||
|
|
@ -836,6 +836,103 @@ test("US-114 see which listings are unread", async ({seed, as}) => {
|
|||
await expect(unreadDot(his)).toHaveCount(0)
|
||||
})
|
||||
|
||||
const seedPoll = (space: SeededSpace, user: TestUser, title: string, createdAt: number) =>
|
||||
space.event(
|
||||
user,
|
||||
() =>
|
||||
space
|
||||
.kind(Poll)
|
||||
.writer()
|
||||
.setTitle(title)
|
||||
.setPollType("singlechoice")
|
||||
.addOption("Yes", "poll-yes")
|
||||
.addOption("No", "poll-no")
|
||||
.setUrls([space.url])
|
||||
.renderTemplate(),
|
||||
createdAt,
|
||||
)
|
||||
|
||||
test("US-123 find a section whose newest item is older than the sync window", async ({
|
||||
seed,
|
||||
as,
|
||||
}) => {
|
||||
const scenario = await seed(({relay, user, at}) => {
|
||||
const space = relay("space")
|
||||
|
||||
space.room("general", {name: "General"})
|
||||
space.join(user.alice, "general")
|
||||
space.join(user.bob, "general")
|
||||
|
||||
// Older than the month the space sync asks for, so the nav can only know about it by asking
|
||||
// the relay what the space holds rather than by reading what has loaded.
|
||||
seedPoll(space, user.alice, "which sextant should we buy", at(60, DAY))
|
||||
})
|
||||
|
||||
const space = scenario.space("space")
|
||||
const bob = await as(users.bob, roomPath(space.url, "general"))
|
||||
|
||||
const pollsNav = contentNavItem(bob, "Polls")
|
||||
|
||||
await expect(pollsNav).toBeVisible()
|
||||
|
||||
await pollsNav.click()
|
||||
|
||||
await expect(bob.getByText("which sextant should we buy")).toBeVisible()
|
||||
})
|
||||
|
||||
test("US-124 reach a badge raised by content the space doesn't have", async ({seed, as}) => {
|
||||
const scenario = await seed(({relay, user, at}) => {
|
||||
const space = relay("space")
|
||||
const other = relay("other")
|
||||
|
||||
space.room("general", {name: "General"})
|
||||
other.room("general", {name: "General"})
|
||||
space.join(user.alice, "general")
|
||||
space.join(user.bob, "general")
|
||||
other.join(user.bob, "general")
|
||||
|
||||
// A comment on a poll nothing holds — the relay kept the comment and dropped its subject.
|
||||
// It counts toward the space either way, so the section it belongs to has to be reachable.
|
||||
space.event(
|
||||
user.alice,
|
||||
() =>
|
||||
space
|
||||
.kind(Comment)
|
||||
.writer()
|
||||
.setRoot(POLL, "f".repeat(64), users.alice.pubkey)
|
||||
.setContent("the second option, surely")
|
||||
.renderTemplate(),
|
||||
at(2, HOUR),
|
||||
)
|
||||
})
|
||||
|
||||
const space = scenario.space("space")
|
||||
const other = scenario.space("other")
|
||||
|
||||
// A space hides its own dot while you are in it, so the badge is only readable from elsewhere
|
||||
const bob = await as(users.bob, roomPath(other.url, "general"))
|
||||
const spaceButton = spaceNavItem(bob, space.name)
|
||||
|
||||
await expect(unreadDot(spaceButton)).toBeVisible()
|
||||
|
||||
await spaceButton.click()
|
||||
|
||||
const pollsNav = contentNavItem(bob, "Polls")
|
||||
|
||||
await expect(pollsNav).toBeVisible()
|
||||
await expect(unreadDot(pollsNav)).toBeVisible()
|
||||
|
||||
await pollsNav.click()
|
||||
|
||||
await expect(bob.getByText("No polls found.")).toBeVisible()
|
||||
|
||||
// Reading it is the end of it: with nothing unread and no poll to list, the space stops
|
||||
// offering the section at all
|
||||
await roomLink(bob, "General").click()
|
||||
|
||||
await expect(pollsNav).toHaveCount(0)
|
||||
})
|
||||
|
||||
test("US-120 read what a notification says", async ({seed, as}) => {
|
||||
const scenario = await seed(({relay, user, at}) => {
|
||||
const space = relay("space")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import {page} from "$app/stores"
|
||||
import {getJson, setJson} from "@welshman/lib"
|
||||
import {EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED, PINBOARD, POLL, LONG_FORM} from "@welshman/util"
|
||||
import {deriveDeduplicatedByValue} from "@welshman/store"
|
||||
|
|
@ -22,7 +23,7 @@
|
|||
import {deriveSpaceSupportedMethods} from "@app/management"
|
||||
import {deriveEventsForUrl} from "@app/repository"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
import {notifications} from "@app/notifications"
|
||||
import {allNotifications, notifications} from "@app/notifications"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
type Props = {
|
||||
|
|
@ -39,6 +40,7 @@
|
|||
const articlesPath = makeSpacePath(url, "articles")
|
||||
const calendarPath = makeSpacePath(url, "calendar")
|
||||
const pollsPath = makeSpacePath(url, "polls")
|
||||
const libraryPath = makeSpacePath(url, "library")
|
||||
|
||||
// Content events aren't retained across page loads, so seed with the kinds seen last time
|
||||
// to keep the nav from re-populating as they load in the background.
|
||||
|
|
@ -50,9 +52,17 @@
|
|||
$events => new Set([...cachedKinds, ...$events.map(e => e.kind)]),
|
||||
)
|
||||
|
||||
// A section is also offered while something under it is unread, or while we're in it. A comment
|
||||
// can name content this space doesn't have, and it counts toward the space either way — hiding
|
||||
// the section it belongs to is what leaves a space lit up with nothing to read.
|
||||
const showSection = (kind: number, path: string) =>
|
||||
$spaceKinds.has(kind) || $allNotifications.has(path) || $page.url.pathname.startsWith(path)
|
||||
|
||||
const hasNip29 = $derived($relay?.hasNip(29) ?? false)
|
||||
const supportedMethods = deriveSpaceSupportedMethods(url)
|
||||
const showLibrary = $derived($spaceKinds.has(PINBOARD) || $supportedMethods.includes("signevent"))
|
||||
const showLibrary = $derived(
|
||||
showSection(PINBOARD, libraryPath) || $supportedMethods.includes("signevent"),
|
||||
)
|
||||
|
||||
const openSearch = () => pushModal(SpaceSearch, {url})
|
||||
|
||||
|
|
@ -71,36 +81,36 @@
|
|||
<Icon icon={UsersGroup} /> Directory
|
||||
</SecondaryNavItem>
|
||||
{#if showLibrary}
|
||||
<SecondaryNavItem href={makeSpacePath(url, "library")}>
|
||||
<SecondaryNavItem href={libraryPath} notification={$notifications.has(libraryPath)}>
|
||||
<Icon icon={GalleryWide} /> Library
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
{#if ENABLE_ZAPS && $spaceKinds.has(ZAP_GOAL)}
|
||||
{#if ENABLE_ZAPS && showSection(ZAP_GOAL, goalsPath)}
|
||||
<SecondaryNavItem href={goalsPath} notification={$notifications.has(goalsPath)}>
|
||||
<Icon icon={StarFallMinimalistic} /> Goals
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
{#if $spaceKinds.has(THREAD)}
|
||||
{#if showSection(THREAD, threadsPath)}
|
||||
<SecondaryNavItem href={threadsPath} notification={$notifications.has(threadsPath)}>
|
||||
<Icon icon={NotesMinimalistic} /> Threads
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
{#if $spaceKinds.has(LONG_FORM)}
|
||||
{#if showSection(LONG_FORM, articlesPath)}
|
||||
<SecondaryNavItem href={articlesPath} notification={$notifications.has(articlesPath)}>
|
||||
<Icon icon={DocumentText} /> Articles
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
{#if $spaceKinds.has(CLASSIFIED)}
|
||||
{#if showSection(CLASSIFIED, classifiedsPath)}
|
||||
<SecondaryNavItem href={classifiedsPath} notification={$notifications.has(classifiedsPath)}>
|
||||
<Icon icon={CaseMinimalistic} /> Classifieds
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
{#if $spaceKinds.has(EVENT_TIME)}
|
||||
{#if showSection(EVENT_TIME, calendarPath)}
|
||||
<SecondaryNavItem href={calendarPath} notification={$notifications.has(calendarPath)}>
|
||||
<Icon icon={CalendarMinimalistic} /> Calendar
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
{#if $spaceKinds.has(POLL)}
|
||||
{#if showSection(POLL, pollsPath)}
|
||||
<SecondaryNavItem href={pollsPath} notification={$notifications.has(pollsPath)}>
|
||||
<Icon icon={Revote} /> Polls
|
||||
</SecondaryNavItem>
|
||||
|
|
|
|||
|
|
@ -290,6 +290,15 @@ const syncSpace = (url: string) => {
|
|||
],
|
||||
})
|
||||
|
||||
// Which sections a space offers is a question about its whole history rather than about the
|
||||
// recent window above — a space whose newest poll is a year old still has polls. One event
|
||||
// per kind answers it.
|
||||
network.get().load({
|
||||
relays: [url],
|
||||
signal: controller.signal,
|
||||
filters: CONTENT_KINDS.map(kind => ({kinds: [kind], limit: 1})),
|
||||
})
|
||||
|
||||
return () => controller.abort()
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue