Let any member curate the space library
This commit is contained in:
parent
f727f2fcd1
commit
218afd48a5
12 changed files with 152 additions and 114 deletions
|
|
@ -57,12 +57,14 @@ plugin, read through `deriveRelaySignedEvents(url, filters)` in `src/app/reposit
|
|||
`src/app/featured.ts` does, rather than a bare `deriveEventsForUrl`.
|
||||
|
||||
Content the space owns is published as the relay: `command.publishAsRelay(url)` has the relay
|
||||
sign the event with its own key through the NIP-86 `signevent` method, then sends it back.
|
||||
Featured content (`setFeaturedContent` in `src/app/featured.ts`) and library shelves and pins
|
||||
(`PinboardEdit`, `PinAdd`, `PinEdit`, `PinMenu`, `BoardMenu`) are written this way, which is why the
|
||||
library lists boards with `Pinboards.forAuthor($relay.self)`. Only users the relay allows to call
|
||||
`signevent` can do it, so `SpaceMenuNavItems` shows the library when the space already has boards or
|
||||
lists `signevent` among its supported methods.
|
||||
sign the event with its own key through the NIP-86 `signevent` method, then sends it back. Featured
|
||||
content (`setFeaturedContent` in `src/app/featured.ts`) is written this way, and only users the
|
||||
relay allows to call `signevent` can write it.
|
||||
|
||||
The library is written by its members. A shelf or a pin is signed with the member's own key and
|
||||
published to the space like any other space content, so the library reads every `PINBOARD` seen on
|
||||
the relay rather than only the relay's own. Whoever signed a shelf is the only one who can edit or
|
||||
delete it, and anyone can pin to it.
|
||||
|
||||
## NIP-29 rooms
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ and NIP-43 tables in [SKILL.md](SKILL.md). Routes are under `src/routes/`.
|
|||
| Classifieds | `CLASSIFIED` (30402) | `Classified` | `src/app/classifieds.ts` | `spaces/[relay]/classifieds`, `classifieds/[address]` (`ClassifiedForm`) |
|
||||
| Goals | `ZAP_GOAL` (9041) | `ZapGoal` | none | `spaces/[relay]/goals`, `goals/[id]` (`GoalCreate`) |
|
||||
| Polls | `POLL` (1068), `POLL_RESPONSE` (1018) | `Poll`, `PollResponse` | none | `spaces/[relay]/polls`, `polls/[id]` (`PollCreate`, `PollVotes`) |
|
||||
| Library | `PINBOARD` (30067), `PIN` (39067) | `Pinboard`, `Pin` | `src/app/pinboards.ts` | `spaces/[relay]/library` (`PinboardEdit`, `PinAdd`); published as the relay |
|
||||
| Library | `PINBOARD` (30067), `PIN` (39067) | `Pinboard`, `Pin` | `src/app/pinboards.ts` | `spaces/[relay]/library` (`PinboardEdit`, `PinAdd`); published by any member |
|
||||
| Room pins | `ROOM_PINS` (39005), `ROOM_UPDATE_PINS` (9010) | `RoomPins`, `RoomUpdatePins` | `src/app/roomPins.ts` | `RoomItemMenu`, `RoomPinnedMessagesAll` |
|
||||
| Featured content | `APP_DATA` (30078), `d` = `flotilla/featured-content` | `AppData` | `src/app/featured.ts` | `SpaceFeaturedContent`; published as the relay |
|
||||
| Bot commands (NIP-CD) | `COMMAND` (31992) | `Command` | `src/app/commands.ts` | `RoomCompose`, `ContentCommand` |
|
||||
|
|
|
|||
|
|
@ -847,11 +847,12 @@ Acceptance:
|
|||
topics match.
|
||||
- Selecting a shelf shows its pins as a gallery; an empty shelf shows a message
|
||||
instead.
|
||||
- As a non-admin, alice sees no "Create Shelf" or "Add a link" controls.
|
||||
- Alice is offered "Create Shelf" and "Add a link", and "Add link" on someone
|
||||
else's shelf, but not that shelf's "Edit shelf" or "Delete shelf".
|
||||
|
||||
### US-054 — Curate the library
|
||||
|
||||
As admin, I want to organize shelves and the links on them, so that members find
|
||||
As bob, I want to organize shelves and the links on them, so that members find
|
||||
good material first.
|
||||
|
||||
Acceptance:
|
||||
|
|
|
|||
|
|
@ -1,10 +1,20 @@
|
|||
import * as nip19 from "nostr-tools/nip19"
|
||||
import {DAY, HOUR, MINUTE, bech32ToHex, int} from "@welshman/lib"
|
||||
import {MESSAGE, POLL_RESPONSE, getLnUrl, tagSpec, tagValues, toMsats} from "@welshman/util"
|
||||
import {
|
||||
MESSAGE,
|
||||
POLL_RESPONSE,
|
||||
getAddress,
|
||||
getLnUrl,
|
||||
tagSpec,
|
||||
tagValues,
|
||||
toMsats,
|
||||
} from "@welshman/util"
|
||||
import type {SignedEvent} from "@welshman/util"
|
||||
import {ClientMessageType} from "@welshman/net"
|
||||
import {
|
||||
Comment,
|
||||
Pin,
|
||||
Pinboard,
|
||||
Poll,
|
||||
PollResponse,
|
||||
Profile,
|
||||
|
|
@ -836,6 +846,8 @@ test("US-052 comment on and react to community posts", async ({seed, as}) => {
|
|||
})
|
||||
|
||||
test("US-053 browse and search the library", async ({seed, as}) => {
|
||||
let gettingStarted!: Seeded
|
||||
|
||||
const scenario = await seed(({relay, user, at}) => {
|
||||
const space = relay("space")
|
||||
|
||||
|
|
@ -845,43 +857,53 @@ test("US-053 browse and search the library", async ({seed, as}) => {
|
|||
space.profile(user.admin, {name: "Ada Admin"})
|
||||
space.profile(user.alice, {name: "Alice Anderson"})
|
||||
space.message(user.admin, "general", "welcome to the space", at(2, HOUR))
|
||||
|
||||
gettingStarted = space.event(
|
||||
user.admin,
|
||||
() =>
|
||||
space
|
||||
.kind(Pinboard)
|
||||
.writer()
|
||||
.setIdentifier()
|
||||
.setTitle("Getting Started")
|
||||
.setDescription("Reading for new members")
|
||||
.setCollaborative(true)
|
||||
.renderTemplate(),
|
||||
at(2, HOUR),
|
||||
)
|
||||
|
||||
space.event(
|
||||
user.admin,
|
||||
() =>
|
||||
space
|
||||
.kind(Pinboard)
|
||||
.writer()
|
||||
.setIdentifier()
|
||||
.setTitle("Recipes")
|
||||
.setDescription("Food and drink from the kitchen")
|
||||
.setCollaborative(true)
|
||||
.renderTemplate(),
|
||||
at(2, HOUR),
|
||||
)
|
||||
|
||||
space.event(
|
||||
user.admin,
|
||||
() =>
|
||||
space
|
||||
.kind(Pin)
|
||||
.writer()
|
||||
.setIdentifier()
|
||||
.addBoard(getAddress(gettingStarted.event))
|
||||
.setTitle("The Handbook")
|
||||
.setExternal("https://handbook.test/start-here")
|
||||
.renderTemplate(),
|
||||
at(1, HOUR),
|
||||
)
|
||||
})
|
||||
|
||||
const {url} = scenario.space("space")
|
||||
const libraryPath = `${spacePath(url)}/library`
|
||||
|
||||
// A shelf is signed by the relay itself, so nothing this process holds a key for can seed one
|
||||
// and the only way to stand one up is through the admin's own ui.
|
||||
const admin = await as(users.admin, libraryPath)
|
||||
|
||||
const createShelf = async (title: string, description: string) => {
|
||||
await admin.getByRole("button", {name: "Create Shelf"}).click()
|
||||
|
||||
const form = dialog(admin, "Create Shelf")
|
||||
|
||||
await form.getByPlaceholder("Shelf title").fill(title)
|
||||
await form.getByPlaceholder("What's this shelf about?").fill(description)
|
||||
await form.getByRole("button", {name: "Save changes"}).click()
|
||||
await expect(admin.getByRole("alert")).toContainText("Shelf created!")
|
||||
}
|
||||
|
||||
await createShelf("Getting Started", "Reading for new members")
|
||||
await createShelf("Recipes", "Food and drink from the kitchen")
|
||||
|
||||
await expect(admin.getByRole("button", {name: /Getting Started/})).toBeVisible()
|
||||
await expect(admin.getByRole("button", {name: /Recipes/})).toBeVisible()
|
||||
|
||||
await shelfCard(admin, "Getting Started").getByRole("button", {name: "More options"}).click()
|
||||
await admin.getByRole("button", {name: "Add link", exact: true}).click()
|
||||
|
||||
const linkForm = dialog(admin, "Add Link")
|
||||
|
||||
await linkForm.getByPlaceholder("URL or nevent...").fill("https://handbook.test/start-here")
|
||||
await linkForm.getByPlaceholder("Optional title").fill("The Handbook")
|
||||
await linkForm.getByRole("button", {name: "Add link"}).click()
|
||||
|
||||
await expect(admin.getByRole("alert")).toContainText("Link added!")
|
||||
|
||||
const alice = await as(users.alice, libraryPath)
|
||||
const term = alice.getByPlaceholder("Search library...")
|
||||
|
||||
|
|
@ -910,10 +932,17 @@ test("US-053 browse and search the library", async ({seed, as}) => {
|
|||
|
||||
await expect(alice.getByText("This shelf doesn't have any links yet.")).toBeVisible()
|
||||
|
||||
// Curating is the admin's, so none of it is offered to an ordinary member — not even on the
|
||||
// empty shelf where the admin is offered it directly.
|
||||
await expect(alice.getByRole("button", {name: "Create Shelf"})).toHaveCount(0)
|
||||
await expect(alice.getByRole("button", {name: "Add a link"})).toHaveCount(0)
|
||||
// The library is the whole space's, so an ordinary member is offered the same controls the
|
||||
// shelves were made with.
|
||||
await expect(alice.getByRole("button", {name: "Create Shelf"})).toBeVisible()
|
||||
await expect(alice.getByRole("button", {name: "Add a link"})).toBeVisible()
|
||||
|
||||
// Someone else's shelf is hers to add to and not to rewrite.
|
||||
await shelfCard(alice, "Recipes").getByRole("button", {name: "More options"}).click()
|
||||
|
||||
await expect(alice.getByRole("button", {name: "Add link", exact: true})).toBeVisible()
|
||||
await expect(alice.getByRole("button", {name: "Edit shelf"})).toHaveCount(0)
|
||||
await expect(alice.getByRole("button", {name: "Delete shelf"})).toHaveCount(0)
|
||||
})
|
||||
|
||||
test("US-054 curate the library", async ({seed, as}) => {
|
||||
|
|
@ -950,7 +979,9 @@ test("US-054 curate the library", async ({seed, as}) => {
|
|||
const {url} = scenario.space("space")
|
||||
const libraryPath = `${spacePath(url)}/library`
|
||||
const pollPath = `${spacePath(url)}/polls/${poll.id}`
|
||||
const page = await as(users.admin, pollPath)
|
||||
|
||||
// Curating is nobody's privilege here, so this is an ordinary member doing all of it.
|
||||
const page = await as(users.bob, pollPath)
|
||||
|
||||
const pollCard = page
|
||||
.locator(".card.z-feature")
|
||||
|
|
@ -973,7 +1004,7 @@ test("US-054 curate the library", async ({seed, as}) => {
|
|||
await shelfForm.getByPlaceholder("What's this shelf about?").fill("Things worth reading")
|
||||
await shelfForm.getByRole("button", {name: "Save changes"}).click()
|
||||
|
||||
// Creating a shelf lists it and drops the admin straight into it.
|
||||
// Creating a shelf lists it and drops its author straight into it.
|
||||
await expect(page.getByRole("alert")).toContainText("Shelf created!")
|
||||
await expect(page).toHaveURL(/[?&]board=/)
|
||||
await expect(page.getByRole("button", {name: /Reading List/})).toHaveAttribute(
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@
|
|||
import EventInfo from "@app/components/EventInfo.svelte"
|
||||
import PinAdd from "@app/components/PinAdd.svelte"
|
||||
import PinboardEdit from "@app/components/PinboardEdit.svelte"
|
||||
import {deletes} from "@app/core"
|
||||
import {deriveUserIsSpaceAdmin} from "@app/management"
|
||||
import {deletes, relays, user} from "@app/core"
|
||||
import {shareEvent} from "@app/share"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
|
@ -27,8 +26,6 @@
|
|||
|
||||
const {url, board, onClick}: Props = $props()
|
||||
|
||||
const canManage = deriveUserIsSpaceAdmin(url)
|
||||
|
||||
const showInfo = () => pushModal(EventInfo, {url, event: board.event})
|
||||
|
||||
const share = () => shareEvent(url, "Shelf", board.event)
|
||||
|
|
@ -39,8 +36,11 @@
|
|||
|
||||
const deleteBoard = async () => {
|
||||
try {
|
||||
const command = await $deletes.deleteEvent(board.event)
|
||||
const error = await command.publishAsRelay(url).then(thunk => thunk.waitForError())
|
||||
const protect = await $relays.hasNip(url, 70)
|
||||
const command = await $deletes.deleteEvent(board.event, writer =>
|
||||
writer.setProtected(protect),
|
||||
)
|
||||
const error = await command.publishToRelays([url]).waitForError()
|
||||
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: error})
|
||||
|
|
@ -80,13 +80,13 @@
|
|||
Share to chat
|
||||
</Button>
|
||||
</li>
|
||||
{#if $canManage}
|
||||
<li>
|
||||
<Button onclick={addLink}>
|
||||
<Icon icon={AddCircle} />
|
||||
Add link
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button onclick={addLink}>
|
||||
<Icon icon={AddCircle} />
|
||||
Add link
|
||||
</Button>
|
||||
</li>
|
||||
{#if board.event.pubkey === $user.pubkey}
|
||||
<li>
|
||||
<Button onclick={edit}>
|
||||
<Icon icon={Pen} />
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
<script lang="ts">
|
||||
import {relay} from "@welshman/util"
|
||||
import {Pin} from "@welshman/domain"
|
||||
import {publishAsRelay} from "@welshman/app"
|
||||
import {publish} from "@welshman/app"
|
||||
import PinForm, {type PinFormValues} from "@app/components/PinForm.svelte"
|
||||
import {command, writer} from "@app/core"
|
||||
import {command, relays, writer} from "@app/core"
|
||||
import {setPinReference} from "@app/pinboards"
|
||||
|
||||
type Props = {
|
||||
|
|
@ -14,7 +15,11 @@
|
|||
const {url, address, reference}: Props = $props()
|
||||
|
||||
const submit = async ({title, topics, value, content}: PinFormValues) => {
|
||||
const eventWriter = writer(Pin).setIdentifier().addBoard(address)
|
||||
const eventWriter = writer(Pin)
|
||||
.setIdentifier()
|
||||
.addBoard(address)
|
||||
.setProtected(await $relays.hasNip(url, 70))
|
||||
.forceRoutes(relay(url))
|
||||
|
||||
if (!setPinReference(eventWriter, value)) {
|
||||
return "Please enter a valid URL or nostr link."
|
||||
|
|
@ -22,7 +27,7 @@
|
|||
|
||||
eventWriter.setTitle(title).setTopics(topics).setContent(content)
|
||||
|
||||
const thunk = await command(eventWriter).then(publishAsRelay(url))
|
||||
const thunk = await command(eventWriter).then(publish)
|
||||
|
||||
return thunk.waitForError()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
<script lang="ts">
|
||||
import {relay} from "@welshman/util"
|
||||
import {Pin} from "@welshman/domain"
|
||||
import {publishAsRelay} from "@welshman/app"
|
||||
import {publish} from "@welshman/app"
|
||||
import type {PinReader} from "@welshman/domain"
|
||||
import PinForm, {type PinFormValues} from "@app/components/PinForm.svelte"
|
||||
import {command, writer} from "@app/core"
|
||||
import {command, relays, writer} from "@app/core"
|
||||
import {pinToReference, setPinReference} from "@app/pinboards"
|
||||
|
||||
type Props = {
|
||||
|
|
@ -15,6 +16,8 @@
|
|||
|
||||
const submit = async ({title, topics, value, content}: PinFormValues) => {
|
||||
const eventWriter = writer(Pin, pin)
|
||||
.setProtected(await $relays.hasNip(url, 70))
|
||||
.forceRoutes(relay(url))
|
||||
|
||||
if (!setPinReference(eventWriter, value)) {
|
||||
return "Please enter a valid URL or nostr link."
|
||||
|
|
@ -22,7 +25,7 @@
|
|||
|
||||
eventWriter.setTitle(title).setTopics(topics).setContent(content)
|
||||
|
||||
const thunk = await command(eventWriter).then(publishAsRelay(url))
|
||||
const thunk = await command(eventWriter).then(publish)
|
||||
|
||||
return thunk.waitForError()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@
|
|||
import Confirm from "@lib/components/Confirm.svelte"
|
||||
import EventInfo from "@app/components/EventInfo.svelte"
|
||||
import PinEdit from "@app/components/PinEdit.svelte"
|
||||
import {deletes} from "@app/core"
|
||||
import {deriveUserIsSpaceAdmin} from "@app/management"
|
||||
import {deletes, relays, user} from "@app/core"
|
||||
import {shareEvent} from "@app/share"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
|
@ -25,8 +24,6 @@
|
|||
|
||||
const {url, pin, onClick}: Props = $props()
|
||||
|
||||
const canManage = deriveUserIsSpaceAdmin(url)
|
||||
|
||||
const showInfo = () => pushModal(EventInfo, {url, event: pin.event})
|
||||
|
||||
const share = () => shareEvent(url, "Link", pin.event)
|
||||
|
|
@ -35,8 +32,9 @@
|
|||
|
||||
const deletePin = async () => {
|
||||
try {
|
||||
const command = await $deletes.deleteEvent(pin.event)
|
||||
const error = await command.publishAsRelay(url).then(thunk => thunk.waitForError())
|
||||
const protect = await $relays.hasNip(url, 70)
|
||||
const command = await $deletes.deleteEvent(pin.event, writer => writer.setProtected(protect))
|
||||
const error = await command.publishToRelays([url]).waitForError()
|
||||
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: error})
|
||||
|
|
@ -76,7 +74,7 @@
|
|||
Share to chat
|
||||
</Button>
|
||||
</li>
|
||||
{#if $canManage}
|
||||
{#if pin.event.pubkey === $user.pubkey}
|
||||
<li>
|
||||
<Button onclick={edit}>
|
||||
<Icon icon={Pen} />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import {navigate} from "@app/modal"
|
||||
import {getAddress} from "@welshman/util"
|
||||
import {publishAsRelay} from "@welshman/app"
|
||||
import {getAddress, relay} from "@welshman/util"
|
||||
import {publish} from "@welshman/app"
|
||||
import {Pinboard} from "@welshman/domain"
|
||||
import type {PinboardReader} from "@welshman/domain"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import RelayName from "@app/components/RelayName.svelte"
|
||||
import {command, writer} from "@app/core"
|
||||
import {command, relays, writer} from "@app/core"
|
||||
import {makeLibraryPath} from "@app/routes"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
|
|
@ -33,13 +33,17 @@
|
|||
loading = true
|
||||
|
||||
try {
|
||||
const eventWriter = writer(Pinboard, board).setTitle(title).setDescription(description)
|
||||
const eventWriter = writer(Pinboard, board)
|
||||
.setTitle(title)
|
||||
.setDescription(description)
|
||||
.setProtected(await $relays.hasNip(url, 70))
|
||||
.forceRoutes(relay(url))
|
||||
|
||||
if (!board) {
|
||||
eventWriter.setIdentifier()
|
||||
eventWriter.setIdentifier().setCollaborative(true)
|
||||
}
|
||||
|
||||
const thunk = await command(eventWriter).then(publishAsRelay(url))
|
||||
const thunk = await command(eventWriter).then(publish)
|
||||
const error = await thunk.waitForError()
|
||||
|
||||
if (error) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
<script lang="ts">
|
||||
import {derived} from "svelte/store"
|
||||
import {PINBOARD} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Pinboard} from "@welshman/domain"
|
||||
import type {PinboardReader} from "@welshman/domain"
|
||||
import {Pinboards} from "@welshman/app"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
|
|
@ -13,8 +15,9 @@
|
|||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||
import RelayName from "@app/components/RelayName.svelte"
|
||||
import PinAdd from "@app/components/PinAdd.svelte"
|
||||
import {app, relays} from "@app/core"
|
||||
import {reader} from "@app/core"
|
||||
import {eventToReference} from "@app/pinboards"
|
||||
import {deriveEventsForUrl} from "@app/repository"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
|
|
@ -25,9 +28,9 @@
|
|||
|
||||
const {url, event}: Props = $props()
|
||||
|
||||
const relay = $relays.one(url)
|
||||
|
||||
const boards = $derived($app.use(Pinboards).forAuthor($relay?.self ?? "").$)
|
||||
const boards = derived(deriveEventsForUrl(url, [{kinds: [PINBOARD]}]), $events =>
|
||||
$events.map(reader(Pinboard)),
|
||||
)
|
||||
|
||||
const reference = eventToReference(event)
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@
|
|||
import {relays} from "@app/core"
|
||||
import {ENABLE_ZAPS} from "@app/env"
|
||||
import {CONTENT_KINDS} from "@app/content"
|
||||
import {deriveSpaceSupportedMethods} from "@app/management"
|
||||
import {deriveEventsForUrl} from "@app/repository"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
import {allNotifications, notifications} from "@app/notifications"
|
||||
|
|
@ -59,10 +58,6 @@
|
|||
$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(
|
||||
showSection(PINBOARD, libraryPath) || $supportedMethods.includes("signevent"),
|
||||
)
|
||||
|
||||
const openSearch = () => pushModal(SpaceSearch, {url})
|
||||
|
||||
|
|
@ -80,7 +75,7 @@
|
|||
<SecondaryNavItem href={makeSpacePath(url, "directory")}>
|
||||
<Icon icon={UsersGroup} /> Directory
|
||||
</SecondaryNavItem>
|
||||
{#if showLibrary}
|
||||
{#if showSection(PINBOARD, libraryPath)}
|
||||
<SecondaryNavItem href={libraryPath} notification={$notifications.has(libraryPath)}>
|
||||
<Icon icon={GalleryWide} /> Library
|
||||
</SecondaryNavItem>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
import {onMount} from "svelte"
|
||||
import {goto} from "$app/navigation"
|
||||
import {page} from "$app/stores"
|
||||
import {derived} from "svelte/store"
|
||||
import {PIN, PINBOARD} from "@welshman/util"
|
||||
import {Pinboards, Pins} from "@welshman/app"
|
||||
import {Pins} from "@welshman/app"
|
||||
import {Pinboard} from "@welshman/domain"
|
||||
import type {PinboardReader} from "@welshman/domain"
|
||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||
import GalleryWide from "@assets/icons/gallery-wide.svg?dataurl"
|
||||
|
|
@ -19,9 +21,9 @@
|
|||
import PinAdd from "@app/components/PinAdd.svelte"
|
||||
import PinboardItem from "@app/components/PinboardItem.svelte"
|
||||
import PinboardEdit from "@app/components/PinboardEdit.svelte"
|
||||
import {app, network, relays} from "@app/core"
|
||||
import {app, network, reader} from "@app/core"
|
||||
import {decodeRelay} from "@app/relays"
|
||||
import {deriveUserIsSpaceAdmin} from "@app/management"
|
||||
import {deriveEventsForUrl} from "@app/repository"
|
||||
import {makeLibraryPath} from "@app/routes"
|
||||
import {pushModal} from "@app/modal"
|
||||
import type {PageProps} from "./$types"
|
||||
|
|
@ -30,16 +32,14 @@
|
|||
|
||||
const url = decodeRelay(params.relay)
|
||||
|
||||
const relay = $relays.one(url)
|
||||
|
||||
const canManage = deriveUserIsSpaceAdmin(url)
|
||||
|
||||
let term = $state("")
|
||||
let loading = $state(true)
|
||||
|
||||
// Shelves are signed by the relay itself, so the relay's own pubkey scopes them
|
||||
// to this space.
|
||||
const boards = $derived($app.use(Pinboards).forAuthor($relay?.self ?? "").$)
|
||||
// A shelf belongs to the space it was published to, so the relay it was seen on
|
||||
// scopes it rather than the pubkey that signed it.
|
||||
const boards = derived(deriveEventsForUrl(url, [{kinds: [PINBOARD]}]), $events =>
|
||||
$events.map(reader(Pinboard)),
|
||||
)
|
||||
|
||||
const address = $derived($page.url.searchParams.get("board") ?? "")
|
||||
|
||||
|
|
@ -89,12 +89,10 @@
|
|||
<strong>Library</strong>
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
{#if $canManage}
|
||||
<Button class="button button-primary button-sm" onclick={createBoard}>
|
||||
<Icon icon={Add} />
|
||||
Create Shelf
|
||||
</Button>
|
||||
{/if}
|
||||
<Button class="button button-primary button-sm" onclick={createBoard}>
|
||||
<Icon icon={Add} />
|
||||
Create Shelf
|
||||
</Button>
|
||||
{/snippet}
|
||||
</SpaceBar>
|
||||
|
||||
|
|
@ -131,12 +129,10 @@
|
|||
{:else if selected}
|
||||
<div class="flex flex-col items-center gap-4 py-20 text-center">
|
||||
<p class="opacity-70">This shelf doesn't have any links yet.</p>
|
||||
{#if $canManage}
|
||||
<Button class="button button-primary" onclick={addLink}>
|
||||
<Icon icon={AddCircle} />
|
||||
Add a link
|
||||
</Button>
|
||||
{/if}
|
||||
<Button class="button button-primary" onclick={addLink}>
|
||||
<Icon icon={AddCircle} />
|
||||
Add a link
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="py-20 text-center opacity-70">Select a shelf to see what's on it.</p>
|
||||
|
|
|
|||
Loading…
Reference in a new issue