Fix editing featured content, add menu to boards and ability to share boards/pins to chat

This commit is contained in:
Jon Staab 2026-07-08 10:29:56 -07:00
parent 269630efc2
commit 1581d909f0
19 changed files with 319 additions and 107 deletions

View file

@ -0,0 +1,44 @@
<script lang="ts">
import {onMount} from "svelte"
import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import EventInfo from "@app/components/EventInfo.svelte"
import {type PublishedBoard} from "@app/pinboards"
import {shareEventToChat} from "@app/share"
import {pushModal} from "@app/modal"
type Props = {
url: string
board: PublishedBoard
onClick: () => void
}
const {url, board, onClick}: Props = $props()
const showInfo = () => pushModal(EventInfo, {url, event: board.event})
const share = () => shareEventToChat(url, "Shelf", board.event)
let ul: Element
onMount(() => {
ul.addEventListener("click", onClick)
})
</script>
<ul class="menu whitespace-nowrap rounded-2xl bg-surface p-2" bind:this={ul}>
<li>
<Button onclick={showInfo}>
<Icon icon={Code2} />
Shelf details
</Button>
</li>
<li>
<Button onclick={share}>
<Icon icon={ShareCircle} />
Share to chat
</Button>
</li>
</ul>

View file

@ -1,29 +1,25 @@
<script lang="ts">
import {onMount} from "svelte"
import type {Snippet} from "svelte"
import {goto} from "$app/navigation"
import type {TrustedEvent} from "@welshman/util"
import {COMMENT, getAddress, ManagementMethod} from "@welshman/util"
import {pubkey, repository, relaysByUrl, manageRelay} from "@welshman/app"
import {pubkey, repository, manageRelay} from "@welshman/app"
import GalleryWide from "@assets/icons/gallery-wide.svg?dataurl"
import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import Danger from "@assets/icons/danger.svg?dataurl"
import {setKey} from "@lib/implicit"
import Button from "@lib/components/Button.svelte"
import Confirm from "@lib/components/Confirm.svelte"
import Icon from "@lib/components/Icon.svelte"
import EventInfo from "@app/components/EventInfo.svelte"
import Report from "@app/components/Report.svelte"
import EventShare from "@app/components/EventShare.svelte"
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
import PinboardSelect from "@app/components/PinboardSelect.svelte"
import {hasNip29} from "@app/relays"
import {shareEventToChat} from "@app/share"
import {deriveUserIsSpaceAdmin} from "@app/members"
import {pushModal} from "@app/modal"
import {pushToast} from "@app/toast"
import {makeSpaceChatPath} from "@app/routes"
type Props = {
url: string
@ -44,14 +40,7 @@
const addToLibrary = () => pushModal(PinboardSelect, {url, address: getAddress(event)})
const share = async () => {
if (hasNip29($relaysByUrl.get(url))) {
pushModal(EventShare, {url, event})
} else {
setKey("share", event)
goto(makeSpaceChatPath(url))
}
}
const share = () => shareEventToChat(url, noun, event)
const showDelete = () => pushModal(EventDeleteConfirm, {url, event})

View file

@ -6,7 +6,10 @@
import NoteContentClassified from "@app/components/NoteContentClassified.svelte"
import NoteContentGoal from "@app/components/NoteContentGoal.svelte"
import NoteContentPoll from "@app/components/NoteContentPoll.svelte"
import NoteContentPin from "@app/components/NoteContentPin.svelte"
import NoteContentBoard from "@app/components/NoteContentBoard.svelte"
import Content from "@app/components/Content.svelte"
import {PIN, BOARD} from "@app/pinboards"
const props: ComponentProps<typeof Content> = $props()
</script>
@ -22,6 +25,10 @@
<NoteContentGoal {...props} />
{:else if props.event.kind === POLL}
<NoteContentPoll {...props} />
{:else if props.event.kind === PIN}
<NoteContentPin {...props} />
{:else if props.event.kind === BOARD}
<NoteContentBoard {...props} />
{:else}
<Content {...props} />
{/if}

View file

@ -0,0 +1,16 @@
<script lang="ts">
import type {ComponentProps} from "svelte"
import Content from "@app/components/Content.svelte"
import {readBoard} from "@app/pinboards"
const props: ComponentProps<typeof Content> = $props()
const board = readBoard(props.event)
</script>
<div class="flex flex-col">
<strong class="text-lg">{board.title || "Untitled shelf"}</strong>
{#if board.description}
<span class="text-sm opacity-70">{board.description}</span>
{/if}
</div>

View file

@ -0,0 +1,50 @@
<script lang="ts">
import type {ComponentProps} from "svelte"
import * as nip19 from "nostr-tools/nip19"
import {Address} from "@welshman/util"
import Badge from "@lib/components/Badge.svelte"
import Content from "@app/components/Content.svelte"
import PinContentEvent from "@app/components/PinContentEvent.svelte"
import {readPin} from "@app/pinboards"
const props: ComponentProps<typeof Content> = $props()
const pin = readPin(props.event)
// Encode nostr references as nostr: entities (i tags are already urls) so
// Content parses them into rich embeds rather than rendering raw ids/coords.
const content = $derived.by(() => {
const [type, data = ""] = pin.value
if (type === "e") return "nostr:" + nip19.neventEncode({id: data})
if (type === "a") {
const {kind, pubkey, identifier} = Address.from(data)
return "nostr:" + nip19.naddrEncode({kind, pubkey, identifier})
}
return data
})
</script>
<div class="flex h-full flex-col gap-2">
{#if pin.title}
<strong class="truncate min-w-0">{pin.title}</strong>
{/if}
{#if pin.description}
<Content event={{content: pin.description, tags: []}} />
{/if}
{#if pin.value[0] === "i"}
<Content event={{content, tags: []}} />
{:else}
<PinContentEvent value={pin.value[1]} relays={pin.value[2] ? [pin.value[2]] : []} />
{/if}
{#if pin.topics.length > 0}
<div class="mt-auto flex flex-wrap gap-1">
{#each pin.topics as topic (topic)}
<Badge>#{topic}</Badge>
{/each}
</div>
{/if}
</div>

View file

@ -6,7 +6,7 @@
import {goToEvent} from "@app/routes"
type Props = {
url: string
url?: string
value: string
relays: string[]
}

View file

@ -1,10 +1,10 @@
<script lang="ts">
import PinForm, {type PinFormValues} from "@app/components/PinForm.svelte"
import {createPin, referenceToPin, pinToReference, type Pin} from "@app/pinboards"
import {createPin, referenceToPin, pinToReference, type PublishedPin} from "@app/pinboards"
type Props = {
url: string
pin: Pin
pin: PublishedPin
}
const {url, pin}: Props = $props()

View file

@ -1,22 +1,22 @@
<script lang="ts">
import * as nip19 from "nostr-tools/nip19"
import {removeUndefined} from "@welshman/lib"
import {Address} from "@welshman/util"
import MenuButton from "@lib/components/MenuButton.svelte"
import Badge from "@lib/components/Badge.svelte"
import MenuButton from "@lib/components/MenuButton.svelte"
import Content from "@app/components/Content.svelte"
import PinContentEvent from "@app/components/PinContentEvent.svelte"
import PinMenu from "@app/components/PinMenu.svelte"
import {type Pin} from "@app/pinboards"
import {type PublishedPin} from "@app/pinboards"
type Props = {
url: string
pin: Pin
pin: PublishedPin
minimal?: boolean
showMenu?: boolean
class?: string
}
const {url, pin, minimal, showMenu, ...props}: Props = $props()
const {url, pin, minimal, ...props}: Props = $props()
// Encode nostr references as nostr: entities (i tags are already urls) so
// Content parses them into rich embeds rather than rendering raw ids/coords.
@ -35,8 +35,8 @@
})
</script>
<div class="{props.class} relative flex h-full flex-col gap-2">
{#if showMenu}
<div class="{props.class} flex h-full flex-col gap-2 {minimal ? '' : 'card relative'}">
{#if !minimal}
<div class="absolute right-2 top-2 z-feature">
<MenuButton
class="button button-neutral button-sm button-square"
@ -45,9 +45,9 @@
component={PinMenu}
componentProps={{url, pin}} />
</div>
{#if pin.title}
<strong class="truncate min-w-0 pr-8">{pin.title}</strong>
{/if}
{/if}
{#if pin.title}
<strong class="truncate min-w-0 pr-8">{pin.title}</strong>
{/if}
{#if pin.description}
<Content event={{content: pin.description, tags: []}} {url} />
@ -55,10 +55,7 @@
{#if pin.value[0] === "i"}
<Content event={{content, tags: []}} {url} />
{:else}
<PinContentEvent
{url}
value={pin.value[1]}
relays={pin.value[2] ? [pin.value[2], url] : [url]} />
<PinContentEvent {url} value={pin.value[1]} relays={removeUndefined([pin.value[2], url])} />
{/if}
{#if !minimal && pin.topics.length > 0}
<div class="mt-auto flex flex-wrap gap-1">

View file

@ -2,22 +2,34 @@
import {onMount} from "svelte"
import Pen from "@assets/icons/pen.svg?dataurl"
import TrashBin from "@assets/icons/trash-bin-2.svg?dataurl"
import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Confirm from "@lib/components/Confirm.svelte"
import EventInfo from "@app/components/EventInfo.svelte"
import PinEdit from "@app/components/PinEdit.svelte"
import {deletePin, type Pin} from "@app/pinboards"
import {deriveSupportedMethods} from "@app/relays"
import {deletePin, type PublishedPin} from "@app/pinboards"
import {shareEventToChat} from "@app/share"
import {pushModal} from "@app/modal"
import {pushToast} from "@app/toast"
type Props = {
url: string
pin: Pin
pin: PublishedPin
onClick: () => void
}
const {url, pin, onClick}: Props = $props()
const supportedMethods = deriveSupportedMethods(url)
const canManage = $derived($supportedMethods.some(m => (m as string) === "signevent"))
const showInfo = () => pushModal(EventInfo, {url, event: pin.event})
const share = () => shareEventToChat(url, "Link", pin.event)
const edit = () => pushModal(PinEdit, {url, pin})
const confirmDelete = () =>
@ -44,15 +56,29 @@
<ul class="menu whitespace-nowrap rounded-2xl bg-surface p-2" bind:this={ul}>
<li>
<Button onclick={edit}>
<Icon icon={Pen} />
Edit link
<Button onclick={showInfo}>
<Icon icon={Code2} />
Event details
</Button>
</li>
<li>
<Button class="text-error" onclick={confirmDelete}>
<Icon icon={TrashBin} />
Delete link
<Button onclick={share}>
<Icon icon={ShareCircle} />
Share to chat
</Button>
</li>
{#if canManage}
<li>
<Button onclick={edit}>
<Icon icon={Pen} />
Edit link
</Button>
</li>
<li>
<Button class="text-error" onclick={confirmDelete}>
<Icon icon={TrashBin} />
Delete link
</Button>
</li>
{/if}
</ul>

View file

@ -13,7 +13,7 @@
import {goto} from "$app/navigation"
import RelayName from "@app/components/RelayName.svelte"
import {editBoard, type Board} from "@app/pinboards"
import {makeSpacePath} from "@app/routes"
import {makeLibraryPath} from "@app/routes"
import {pushToast} from "@app/toast"
type Props = {
@ -43,7 +43,7 @@
// Navigate to a freshly created board; otherwise just close the modal.
if (isNew) {
goto(makeSpacePath(url, "library", board.address))
goto(makeLibraryPath(url, board.address))
} else {
back()
}

View file

@ -2,30 +2,40 @@
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import Pin from "@app/components/Pin.svelte"
import {derivePins, type Board} from "@app/pinboards"
import {makeSpacePath} from "@app/routes"
import MenuButton from "@lib/components/MenuButton.svelte"
import PinItem from "@app/components/PinItem.svelte"
import BoardMenu from "@app/components/BoardMenu.svelte"
import {derivePins, type PublishedBoard} from "@app/pinboards"
import {makeLibraryPath} from "@app/routes"
type Props = {
url: string
board: Board
board: PublishedBoard
}
const {url, board}: Props = $props()
const pins = derivePins(url, board.address)
const detailPath = makeSpacePath(url, "library", board.address)
const detailPath = makeLibraryPath(url, board.address)
</script>
<div class="card flex flex-col gap-2">
<div class="flex flex-col">
<div class="card relative flex flex-col gap-2">
<div class="absolute right-2 top-2 z-feature">
<MenuButton
class="button button-neutral button-sm button-square"
aria-label="More options"
iconSize={4}
component={BoardMenu}
componentProps={{url, board}} />
</div>
<div class="flex flex-col pr-8">
<strong class="text-lg">{board.title || "Untitled shelf"}</strong>
{#if board.description}
<span class="text-sm opacity-70">{board.description}</span>
{/if}
</div>
{#each $pins.slice(0, 1) as pin (pin.id)}
<Pin minimal {url} {pin} />
<PinItem minimal {url} {pin} />
{/each}
<Link href={detailPath} class="button button-primary">
<Icon icon={AltArrowRight} />

View file

@ -10,7 +10,7 @@
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import RelayName from "@app/components/RelayName.svelte"
import PinAdd from "@app/components/PinAdd.svelte"
import {deriveBoards, addressToReference, type Board} from "@app/pinboards"
import {deriveBoards, addressToReference, type PublishedBoard} from "@app/pinboards"
import {makeSpacePath} from "@app/routes"
import {pushModal} from "@app/modal"
@ -24,7 +24,8 @@
const boards = deriveBoards(url)
const reference = addressToReference(address)
const selectBoard = (board: Board) => pushModal(PinAdd, {url, address: board.address, reference})
const selectBoard = (board: PublishedBoard) =>
pushModal(PinAdd, {url, address: board.address, reference})
</script>
<Modal class="flex flex-col gap-2">

View file

@ -9,16 +9,18 @@
const props: ComponentProps<typeof NoteContent> = $props()
const path = getRoomItemPath(props.url!, props.event)
const minLength = 5000
const maxLength = 5500
const className = cx("text-sm block", {"card card-sm shadow-none": props.event.kind !== MESSAGE})
</script>
<div class={cx("text-sm", {"card card-sm shadow-none": props.event.kind !== MESSAGE})}>
{#if path && !isMobile}
<Link href={path}>
<NoteContent {...props} {minLength} {maxLength} />
</Link>
{:else}
{#if path && !isMobile}
<Link href={path} class={className}>
<NoteContent {...props} {minLength} {maxLength} />
{/if}
</div>
</Link>
{:else}
<div class={className}>
<NoteContent {...props} {minLength} {maxLength} />
</div>
{/if}

View file

@ -1,9 +1,8 @@
import {derived} from "svelte/store"
import {now} from "@welshman/lib"
import {APP_DATA, getTagValues} from "@welshman/util"
import type {ManagementMethod} from "@welshman/util"
import {manageRelay} from "@welshman/app"
import {deriveRelaySignedEvents} from "@app/repository"
import {signAsRelay} from "@app/relays"
// NIP-78 app data published by the relay's self key. Each featured entry is a
// ["content", <value>] tag (freeform text, intended to be a url or nevent).
@ -17,11 +16,8 @@ export const deriveFeaturedContent = (url: string) =>
// Publish the featured content list by asking the relay to sign it with its self
// key (the unofficial NIP-86 "signevent" method).
export const setFeaturedContent = async (
url: string,
content: string[],
): Promise<string | undefined> => {
const template = {
export const setFeaturedContent = (url: string, content: string[]): Promise<string | undefined> =>
signAsRelay(url, {
kind: APP_DATA,
created_at: now(),
content: "",
@ -32,12 +28,4 @@ export const setFeaturedContent = async (
.filter(Boolean)
.map(value => ["content", value]),
],
}
const {error} = await manageRelay(url, {
method: "signevent" as ManagementMethod,
params: [template as unknown as string],
})
return error
}

View file

@ -13,15 +13,16 @@ import {
readList,
sortEventsDesc,
} from "@welshman/util"
import type {ManagementMethod, TrustedEvent} from "@welshman/util"
import {manageRelay} from "@welshman/app"
import type {TrustedEvent} from "@welshman/util"
import {deriveEventsForUrl, deriveRelaySignedEvents} from "@app/repository"
import {signAsRelay} from "@app/relays"
export const BOARD = 30067
export const PIN = 39067
export type Board = {
event?: TrustedEvent
address: string
identifier: string
title: string
@ -31,7 +32,12 @@ export type Board = {
collaborative: boolean
}
export type PublishedBoard = Board & {
event: TrustedEvent
}
export type Pin = {
event?: TrustedEvent
id: string
pubkey: string
identifier: string
@ -44,10 +50,15 @@ export type Pin = {
value: string[]
}
export const readBoard = (event: TrustedEvent): Board => {
export type PublishedPin = Pin & {
event: TrustedEvent
}
export const readBoard = (event: TrustedEvent): PublishedBoard => {
const tags = getListTags(readList(asDecryptedEvent(event)))
return {
event,
address: getAddress(event),
identifier: getTagValue("d", tags) ?? "",
title: getTagValue("title", tags) ?? "",
@ -58,7 +69,8 @@ export const readBoard = (event: TrustedEvent): Board => {
}
}
export const readPin = (event: TrustedEvent): Pin => ({
export const readPin = (event: TrustedEvent): PublishedPin => ({
event,
id: event.id,
pubkey: event.pubkey,
identifier: getTagValue("d", event.tags) ?? "",
@ -85,15 +97,6 @@ export const derivePins = (url: string, address: string) =>
sortEventsDesc($events).map(readPin),
)
const signAsRelay = async (url: string, template: object): Promise<string | undefined> => {
const {error} = await manageRelay(url, {
method: "signevent" as ManagementMethod,
params: [template as unknown as string],
})
return error
}
const makeBoardTemplate = ({
identifier,
title,

View file

@ -6,6 +6,7 @@ import {
getRelaysFromList,
getTagValue,
isShareableRelayUrl,
isSignedEvent,
makeEvent,
normalizeRelayUrl,
} from "@welshman/util"
@ -29,7 +30,9 @@ import {
loadRelay,
manageRelay,
publishThunk,
repository,
sign,
tracker,
waitForThunkError,
} from "@welshman/app"
import {checkRelayHasLivekit} from "$lib/livekit"
@ -116,6 +119,20 @@ export const deriveSocketStatus = (url: string) =>
}),
)
export const signAsRelay = async (url: string, template: object): Promise<string | undefined> => {
const {result, error} = await manageRelay(url, {
method: "signevent" as ManagementMethod,
params: [template as unknown as string],
})
if (!error && isSignedEvent(result)) {
repository.publish(result)
tracker.track(result.id, url)
}
return error
}
export const deriveSupportedMethods = simpleCache(([url]: [string]) => {
return readable<ManagementMethod[]>([], set => {
manageRelay(url, {

View file

@ -24,6 +24,7 @@ import {entityLink, PLATFORM_URL, PLATFORM_RELAYS} from "@app/env"
import {encodeRelay, hasNip29} from "@app/relays"
import {DM_KINDS} from "@app/content"
import {ROOM} from "@app/groups"
import {BOARD} from "@app/pinboards"
import {pushModal} from "@app/modal"
import ChatEnable from "@app/components/ChatEnable.svelte"
@ -131,6 +132,9 @@ export const makeCalendarPath = (url: string, address?: string) =>
export const makePollPath = (url: string, id?: string) => makeSpacePath(url, "polls", id)
export const makeLibraryPath = (url: string, address?: string) =>
makeSpacePath(url, "library", address)
export const scrollToEvent = (id: string) => {
const element = document.querySelector(`[data-event="${id}"]`) as any
@ -187,6 +191,10 @@ export const getEventPath = (event: TrustedEvent, urls: string[]) => {
return makeCalendarPath(url, getAddress(event))
}
if (event.kind === BOARD) {
return makeLibraryPath(url, getAddress(event))
}
if (event.kind === POLL) {
return makePollPath(url, event.id)
}
@ -248,6 +256,8 @@ export const getRoomItemPath = (url: string, event: TrustedEvent) => {
return makeGoalPath(url, event.id)
case EVENT_TIME:
return makeCalendarPath(url, getAddress(event))
case BOARD:
return makeLibraryPath(url, getAddress(event))
case POLL:
return makePollPath(url, event.id)
}

20
src/app/share.ts Normal file
View file

@ -0,0 +1,20 @@
import {get} from "svelte/store"
import {goto} from "$app/navigation"
import type {TrustedEvent} from "@welshman/util"
import {relaysByUrl} from "@welshman/app"
import {setKey} from "@lib/implicit"
import EventShare from "@app/components/EventShare.svelte"
import {hasNip29} from "@app/relays"
import {pushModal} from "@app/modal"
import {makeSpaceChatPath} from "@app/routes"
// Share an event to chat. On NIP-29 relays we prompt for a room to share into;
// otherwise we stash the event and jump to the space chat composer, which quotes it.
export const shareEventToChat = (url: string, noun: string, event: TrustedEvent) => {
if (hasNip29(get(relaysByUrl).get(url))) {
pushModal(EventShare, {url, noun, event})
} else {
setKey("share", event)
goto(makeSpaceChatPath(url))
}
}

View file

@ -11,6 +11,8 @@
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
import Pen from "@assets/icons/pen.svg?dataurl"
import TrashBin from "@assets/icons/trash-bin-2.svg?dataurl"
import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Popover from "@lib/components/Popover.svelte"
@ -19,11 +21,13 @@
import Masonry from "@lib/components/Masonry.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import SpaceBar from "@app/components/SpaceBar.svelte"
import Pin from "@app/components/Pin.svelte"
import PinItem from "@app/components/PinItem.svelte"
import EventInfo from "@app/components/EventInfo.svelte"
import PinboardEdit from "@app/components/PinboardEdit.svelte"
import PinAdd from "@app/components/PinAdd.svelte"
import {decodeRelay, deriveSupportedMethods} from "@app/relays"
import {deriveBoardByAddress, derivePins, deleteBoard, PIN} from "@app/pinboards"
import {shareEventToChat} from "@app/share"
import {pushModal} from "@app/modal"
import {pushToast} from "@app/toast"
@ -52,6 +56,22 @@
const back = () => history.back()
const showInfo = () => {
menuOpen = false
if ($board) {
pushModal(EventInfo, {url, event: $board.event})
}
}
const share = () => {
menuOpen = false
if ($board) {
shareEventToChat(url, "Shelf", $board.event)
}
}
const addLink = () => {
menuOpen = false
pushModal(PinAdd, {url, address})
@ -98,19 +118,31 @@
<h1 class="truncate text-xl">{$board?.title || "Shelf"}</h1>
{/snippet}
{#snippet action()}
{#if canManage}
<div class="relative">
<Button
class="button button-neutral button-sm button-square"
aria-label="More options"
onclick={() => (menuOpen = !menuOpen)}>
<Icon size={4} icon={MenuDots} />
</Button>
{#if menuOpen}
<Popover hideOnClick onClose={() => (menuOpen = false)}>
<ul
transition:fly
class="menu bg-surface absolute right-0 z-popover mt-2 w-48 gap-1 rounded-2xl p-2">
<div class="relative">
<Button
class="button button-neutral button-sm button-square"
aria-label="More options"
onclick={() => (menuOpen = !menuOpen)}>
<Icon size={4} icon={MenuDots} />
</Button>
{#if menuOpen}
<Popover hideOnClick onClose={() => (menuOpen = false)}>
<ul
transition:fly
class="menu bg-surface absolute right-0 z-popover mt-2 w-48 gap-1 rounded-2xl p-2">
<li>
<Button onclick={showInfo}>
<Icon icon={Code2} />
Event details
</Button>
</li>
<li>
<Button onclick={share}>
<Icon icon={ShareCircle} />
Share to chat
</Button>
</li>
{#if canManage}
<li>
<Button onclick={addLink}>
<Icon icon={AddCircle} />
@ -129,11 +161,11 @@
Delete shelf
</Button>
</li>
</ul>
</Popover>
{/if}
</div>
{/if}
{/if}
</ul>
</Popover>
{/if}
</div>
{/snippet}
</SpaceBar>
@ -159,7 +191,7 @@
{:else}
<Masonry items={filtered} getKey={pin => pin.id} columnWidth={60} gap={3}>
{#snippet child(pin)}
<Pin {url} {pin} showMenu={canManage} class="card" />
<PinItem {url} {pin} />
{/snippet}
</Masonry>
{/if}