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}