From 1581d909f0b3e238597ff5b998141b15aecdab9b Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Wed, 8 Jul 2026 10:29:56 -0700 Subject: [PATCH] Fix editing featured content, add menu to boards and ability to share boards/pins to chat --- src/app/components/BoardMenu.svelte | 44 ++++++++++++ src/app/components/EventMenu.svelte | 17 +---- src/app/components/NoteContent.svelte | 7 ++ src/app/components/NoteContentBoard.svelte | 16 +++++ src/app/components/NoteContentPin.svelte | 50 +++++++++++++ src/app/components/PinContentEvent.svelte | 2 +- src/app/components/PinEdit.svelte | 4 +- .../components/{Pin.svelte => PinItem.svelte} | 25 +++---- src/app/components/PinMenu.svelte | 42 ++++++++--- src/app/components/PinboardEdit.svelte | 4 +- src/app/components/PinboardItem.svelte | 26 ++++--- src/app/components/PinboardSelect.svelte | 5 +- src/app/components/RoomItemContent.svelte | 18 ++--- src/app/featured.ts | 18 +---- src/app/pinboards.ts | 29 ++++---- src/app/relays.ts | 17 +++++ src/app/routes.ts | 10 +++ src/app/share.ts | 20 ++++++ .../[relay]/library/[address]/+page.svelte | 72 +++++++++++++------ 19 files changed, 319 insertions(+), 107 deletions(-) create mode 100644 src/app/components/BoardMenu.svelte create mode 100644 src/app/components/NoteContentBoard.svelte create mode 100644 src/app/components/NoteContentPin.svelte rename src/app/components/{Pin.svelte => PinItem.svelte} (78%) create mode 100644 src/app/share.ts diff --git a/src/app/components/BoardMenu.svelte b/src/app/components/BoardMenu.svelte new file mode 100644 index 00000000..7c4481a0 --- /dev/null +++ b/src/app/components/BoardMenu.svelte @@ -0,0 +1,44 @@ + + + diff --git a/src/app/components/EventMenu.svelte b/src/app/components/EventMenu.svelte index a82c52fe..aed61d70 100644 --- a/src/app/components/EventMenu.svelte +++ b/src/app/components/EventMenu.svelte @@ -1,29 +1,25 @@ @@ -22,6 +25,10 @@ {:else if props.event.kind === POLL} + {:else if props.event.kind === PIN} + + {:else if props.event.kind === BOARD} + {:else} {/if} diff --git a/src/app/components/NoteContentBoard.svelte b/src/app/components/NoteContentBoard.svelte new file mode 100644 index 00000000..22664ac7 --- /dev/null +++ b/src/app/components/NoteContentBoard.svelte @@ -0,0 +1,16 @@ + + +
+ {board.title || "Untitled shelf"} + {#if board.description} + {board.description} + {/if} +
diff --git a/src/app/components/NoteContentPin.svelte b/src/app/components/NoteContentPin.svelte new file mode 100644 index 00000000..cf1e6097 --- /dev/null +++ b/src/app/components/NoteContentPin.svelte @@ -0,0 +1,50 @@ + + +
+ {#if pin.title} + {pin.title} + {/if} + {#if pin.description} + + {/if} + {#if pin.value[0] === "i"} + + {:else} + + {/if} + {#if pin.topics.length > 0} +
+ {#each pin.topics as topic (topic)} + #{topic} + {/each} +
+ {/if} +
diff --git a/src/app/components/PinContentEvent.svelte b/src/app/components/PinContentEvent.svelte index 32753d05..0e39a97f 100644 --- a/src/app/components/PinContentEvent.svelte +++ b/src/app/components/PinContentEvent.svelte @@ -6,7 +6,7 @@ import {goToEvent} from "@app/routes" type Props = { - url: string + url?: string value: string relays: string[] } diff --git a/src/app/components/PinEdit.svelte b/src/app/components/PinEdit.svelte index d823bf27..a3c17e5f 100644 --- a/src/app/components/PinEdit.svelte +++ b/src/app/components/PinEdit.svelte @@ -1,10 +1,10 @@ -
- {#if showMenu} +
+ {#if !minimal}
- {#if pin.title} - {pin.title} - {/if} + {/if} + {#if pin.title} + {pin.title} {/if} {#if pin.description} @@ -55,10 +55,7 @@ {#if pin.value[0] === "i"} {:else} - + {/if} {#if !minimal && pin.topics.length > 0}
diff --git a/src/app/components/PinMenu.svelte b/src/app/components/PinMenu.svelte index 2253454e..9798a867 100644 --- a/src/app/components/PinMenu.svelte +++ b/src/app/components/PinMenu.svelte @@ -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 @@ diff --git a/src/app/components/PinboardEdit.svelte b/src/app/components/PinboardEdit.svelte index 005cbf5d..936a196d 100644 --- a/src/app/components/PinboardEdit.svelte +++ b/src/app/components/PinboardEdit.svelte @@ -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() } diff --git a/src/app/components/PinboardItem.svelte b/src/app/components/PinboardItem.svelte index 1561265a..0f1052b9 100644 --- a/src/app/components/PinboardItem.svelte +++ b/src/app/components/PinboardItem.svelte @@ -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) -
-
+
+
+ +
+
{board.title || "Untitled shelf"} {#if board.description} {board.description} {/if}
{#each $pins.slice(0, 1) as pin (pin.id)} - + {/each} diff --git a/src/app/components/PinboardSelect.svelte b/src/app/components/PinboardSelect.svelte index edcc2463..b57f9eaf 100644 --- a/src/app/components/PinboardSelect.svelte +++ b/src/app/components/PinboardSelect.svelte @@ -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}) diff --git a/src/app/components/RoomItemContent.svelte b/src/app/components/RoomItemContent.svelte index 9447f33c..6adf9a6d 100644 --- a/src/app/components/RoomItemContent.svelte +++ b/src/app/components/RoomItemContent.svelte @@ -9,16 +9,18 @@ const props: ComponentProps = $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}) -
- {#if path && !isMobile} - - - - {:else} +{#if path && !isMobile} + - {/if} -
+ +{:else} +
+ +
+{/if} diff --git a/src/app/featured.ts b/src/app/featured.ts index 97409e9d..c7f50d5d 100644 --- a/src/app/featured.ts +++ b/src/app/featured.ts @@ -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", ] 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 => { - const template = { +export const setFeaturedContent = (url: string, content: string[]): Promise => + 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 -} diff --git a/src/app/pinboards.ts b/src/app/pinboards.ts index 3ac671bc..606531df 100644 --- a/src/app/pinboards.ts +++ b/src/app/pinboards.ts @@ -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 => { - const {error} = await manageRelay(url, { - method: "signevent" as ManagementMethod, - params: [template as unknown as string], - }) - - return error -} - const makeBoardTemplate = ({ identifier, title, diff --git a/src/app/relays.ts b/src/app/relays.ts index 0e6bf73d..efdaa444 100644 --- a/src/app/relays.ts +++ b/src/app/relays.ts @@ -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 => { + 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([], set => { manageRelay(url, { diff --git a/src/app/routes.ts b/src/app/routes.ts index 81b4abdd..722cb6d8 100644 --- a/src/app/routes.ts +++ b/src/app/routes.ts @@ -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) } diff --git a/src/app/share.ts b/src/app/share.ts new file mode 100644 index 00000000..08819721 --- /dev/null +++ b/src/app/share.ts @@ -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)) + } +} diff --git a/src/routes/spaces/[relay]/library/[address]/+page.svelte b/src/routes/spaces/[relay]/library/[address]/+page.svelte index ea38068e..2c379858 100644 --- a/src/routes/spaces/[relay]/library/[address]/+page.svelte +++ b/src/routes/spaces/[relay]/library/[address]/+page.svelte @@ -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 @@

{$board?.title || "Shelf"}

{/snippet} {#snippet action()} - {#if canManage} -
- - {#if menuOpen} - (menuOpen = false)}> - + + {/if} +
{/snippet} @@ -159,7 +191,7 @@ {:else} pin.id} columnWidth={60} gap={3}> {#snippet child(pin)} - + {/snippet} {/if}