Fix pin references for non-addressable events, malformed shelf addresses, and profile links

This commit is contained in:
Jon Staab 2026-07-27 15:16:21 -07:00
parent 883528242c
commit 0f17e62e54
7 changed files with 50 additions and 25 deletions

View file

@ -2,7 +2,7 @@
import {onMount} from "svelte" import {onMount} from "svelte"
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {COMMENT, getAddress, ManagementMethod} from "@welshman/util" import {COMMENT, ManagementMethod} from "@welshman/util"
import {pubkey, repository, manageRelay} from "@welshman/app" import {pubkey, repository, manageRelay} from "@welshman/app"
import GalleryWide from "@assets/icons/gallery-wide.svg?dataurl" import GalleryWide from "@assets/icons/gallery-wide.svg?dataurl"
import ShareCircle from "@assets/icons/share-circle.svg?dataurl" import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
@ -38,7 +38,7 @@
const showInfo = () => pushModal(EventInfo, {url, event}) const showInfo = () => pushModal(EventInfo, {url, event})
const addToLibrary = () => pushModal(PinboardSelect, {url, address: getAddress(event)}) const addToLibrary = () => pushModal(PinboardSelect, {url, event})
const share = () => shareEventToChat(url, noun, event) const share = () => shareEventToChat(url, noun, event)

View file

@ -18,6 +18,8 @@
if (type === "e") return "nostr:" + nip19.neventEncode({id: data}) if (type === "e") return "nostr:" + nip19.neventEncode({id: data})
if (type === "p") return "nostr:" + nip19.nprofileEncode({pubkey: data})
if (type === "a") { if (type === "a") {
const {kind, pubkey, identifier} = Address.from(data) const {kind, pubkey, identifier} = Address.from(data)
@ -35,10 +37,10 @@
{#if pin.description} {#if pin.description}
<Content event={{content: pin.description, tags: []}} /> <Content event={{content: pin.description, tags: []}} />
{/if} {/if}
{#if pin.value[0] === "i"} {#if pin.value[0] === "e" || pin.value[0] === "a"}
<Content event={{content, tags: []}} />
{:else}
<PinContentEvent value={pin.value[1]} relays={pin.value[2] ? [pin.value[2]] : []} /> <PinContentEvent value={pin.value[1]} relays={pin.value[2] ? [pin.value[2]] : []} />
{:else}
<Content event={{content, tags: []}} />
{/if} {/if}
{#if pin.topics.length > 0} {#if pin.topics.length > 0}
<div class="mt-auto flex flex-wrap gap-1"> <div class="mt-auto flex flex-wrap gap-1">

View file

@ -130,7 +130,7 @@
Link Link
{/snippet} {/snippet}
{#snippet info()} {#snippet info()}
<p>A URL or nostr link (note, nevent, or naddr).</p> <p>A URL or nostr link (note, nevent, naddr, npub, or nprofile).</p>
{/snippet} {/snippet}
{#snippet input()} {#snippet input()}
<input bind:value class="input input-bordered w-full" placeholder="URL or nevent..." /> <input bind:value class="input input-bordered w-full" placeholder="URL or nevent..." />

View file

@ -25,6 +25,8 @@
if (type === "e") return "nostr:" + nip19.neventEncode({id: data, relays: [url]}) if (type === "e") return "nostr:" + nip19.neventEncode({id: data, relays: [url]})
if (type === "p") return "nostr:" + nip19.nprofileEncode({pubkey: data, relays: [url]})
if (type === "a") { if (type === "a") {
const {kind, pubkey, identifier} = Address.from(data) const {kind, pubkey, identifier} = Address.from(data)
@ -52,10 +54,10 @@
{#if pin.description} {#if pin.description}
<Content event={{content: pin.description, tags: []}} {url} /> <Content event={{content: pin.description, tags: []}} {url} />
{/if} {/if}
{#if pin.value[0] === "i"} {#if pin.value[0] === "e" || pin.value[0] === "a"}
<Content event={{content, tags: []}} {url} />
{:else}
<PinContentEvent {url} value={pin.value[1]} relays={removeUndefined([pin.value[2], url])} /> <PinContentEvent {url} value={pin.value[1]} relays={removeUndefined([pin.value[2], url])} />
{:else}
<Content event={{content, tags: []}} {url} />
{/if} {/if}
{#if !minimal && pin.topics.length > 0} {#if !minimal && pin.topics.length > 0}
<div class="mt-auto flex flex-wrap gap-1"> <div class="mt-auto flex flex-wrap gap-1">

View file

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import type {TrustedEvent} from "@welshman/util"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@ -10,19 +11,19 @@
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte" import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import RelayName from "@app/components/RelayName.svelte" import RelayName from "@app/components/RelayName.svelte"
import PinAdd from "@app/components/PinAdd.svelte" import PinAdd from "@app/components/PinAdd.svelte"
import {deriveBoards, addressToReference, type PublishedBoard} from "@app/pinboards" import {deriveBoards, eventToReference, type PublishedBoard} from "@app/pinboards"
import {makeSpacePath} from "@app/routes" import {makeSpacePath} from "@app/routes"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
type Props = { type Props = {
url: string url: string
address: string event: TrustedEvent
} }
const {url, address}: Props = $props() const {url, event}: Props = $props()
const boards = deriveBoards(url) const boards = deriveBoards(url)
const reference = addressToReference(address) const reference = eventToReference(event)
const selectBoard = (board: PublishedBoard) => const selectBoard = (board: PublishedBoard) =>
pushModal(PinAdd, {url, address: board.address, reference}) pushModal(PinAdd, {url, address: board.address, reference})

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {ManagementMethod, getAddress, getTagValue} from "@welshman/util" import {ManagementMethod, getTagValue} from "@welshman/util"
import {pubkey, manageRelay, repository} from "@welshman/app" import {pubkey, manageRelay, repository} from "@welshman/app"
import Code2 from "@assets/icons/code-2.svg?dataurl" import Code2 from "@assets/icons/code-2.svg?dataurl"
import GalleryWide from "@assets/icons/gallery-wide.svg?dataurl" import GalleryWide from "@assets/icons/gallery-wide.svg?dataurl"
@ -36,7 +36,7 @@
const addToLibrary = () => { const addToLibrary = () => {
onClick() onClick()
pushModal(PinboardSelect, {url, address: getAddress(event)}) pushModal(PinboardSelect, {url, event})
} }
const report = () => { const report = () => {

View file

@ -1,4 +1,5 @@
import {derived} from "svelte/store" import {derived, readable} from "svelte/store"
import type {Readable} from "svelte/store"
import * as nip19 from "nostr-tools/nip19" import * as nip19 from "nostr-tools/nip19"
import {now, randomId} from "@welshman/lib" import {now, randomId} from "@welshman/lib"
import { import {
@ -10,10 +11,12 @@ import {
getListTags, getListTags,
getTagValue, getTagValue,
getTagValues, getTagValues,
isReplaceableKind,
readList, readList,
sortEventsDesc, sortEventsDesc,
} from "@welshman/util" } from "@welshman/util"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {isLink, parse} from "@welshman/content"
import {deriveEventsForUrl, deriveRelaySignedEvents} from "@app/repository" import {deriveEventsForUrl, deriveRelaySignedEvents} from "@app/repository"
import {signAsRelay} from "@app/relays" import {signAsRelay} from "@app/relays"
@ -45,8 +48,9 @@ export type Pin = {
description: string description: string
title: string title: string
topics: string[] topics: string[]
// The pin's content reference: the first e/a/i tag found, e.g. ["e", <id>], // The pin's content reference: the first e/a/p/i tag found, e.g. ["e", <id>],
// ["a", <coord>], or ["i", <external id>]. Empty when the pin has no reference. // ["a", <coord>], ["p", <pubkey>], or ["i", <url>]. Empty when the pin has no
// reference.
value: string[] value: string[]
} }
@ -78,7 +82,7 @@ export const readPin = (event: TrustedEvent): PublishedPin => ({
description: event.content, description: event.content,
title: getTagValue("title", event.tags) ?? "", title: getTagValue("title", event.tags) ?? "",
topics: getTagValues("t", event.tags), topics: getTagValues("t", event.tags),
value: event.tags.find(tag => ["e", "a", "i"].includes(tag[0])) ?? [], value: event.tags.find(tag => ["e", "a", "p", "i"].includes(tag[0])) ?? [],
}) })
export const deriveBoards = (url: string) => export const deriveBoards = (url: string) =>
@ -89,8 +93,13 @@ export const deriveBoard = (url: string, identifier: string) =>
event ? readBoard(event) : undefined, event ? readBoard(event) : undefined,
) )
export const deriveBoardByAddress = (url: string, address: string) => export const deriveBoardByAddress = (
deriveBoard(url, Address.from(address).identifier) url: string,
address: string,
): Readable<PublishedBoard | undefined> =>
Address.isAddress(address)
? deriveBoard(url, Address.from(address).identifier)
: readable(undefined)
export const derivePins = (url: string, address: string) => export const derivePins = (url: string, address: string) =>
derived(deriveRelaySignedEvents(url, [{kinds: [PIN], "#A": [address]}]), $events => derived(deriveRelaySignedEvents(url, [{kinds: [PIN], "#A": [address]}]), $events =>
@ -175,6 +184,8 @@ const valueToReference = (value: string[]): string => {
if (type === "e") return nip19.neventEncode({id: data, relays}) if (type === "e") return nip19.neventEncode({id: data, relays})
if (type === "p") return nip19.nprofileEncode({pubkey: data, relays})
if (type === "a") { if (type === "a") {
const {kind, pubkey, identifier} = Address.from(data) const {kind, pubkey, identifier} = Address.from(data)
@ -186,31 +197,40 @@ const valueToReference = (value: string[]): string => {
export const pinToReference = (pin: Pin): string => valueToReference(pin.value) export const pinToReference = (pin: Pin): string => valueToReference(pin.value)
export const addressToReference = (address: string): string => valueToReference(["a", address]) export const eventToReference = (event: TrustedEvent): string =>
valueToReference(isReplaceableKind(event.kind) ? ["a", getAddress(event)] : ["e", event.id])
export const referenceToPin = (reference: string): Partial<Pin> | undefined => { export const referenceToPin = (reference: string): Partial<Pin> | undefined => {
const trimmed = reference.trim() const trimmed = reference.trim()
if (!trimmed) return undefined
try { try {
const decoded = nip19.decode(fromNostrURI(trimmed)) const decoded = nip19.decode(fromNostrURI(trimmed))
if (decoded.type === "note") return {value: ["e", decoded.data]} if (decoded.type === "note") return {value: ["e", decoded.data]}
if (decoded.type === "npub") return {value: ["p", decoded.data]}
if (decoded.type === "nevent") { if (decoded.type === "nevent") {
const {id, relays} = decoded.data const {id, relays} = decoded.data
return {value: relays?.[0] ? ["e", id, relays[0]] : ["e", id]} return {value: relays?.[0] ? ["e", id, relays[0]] : ["e", id]}
} }
if (decoded.type === "nprofile") {
const {pubkey, relays} = decoded.data
return {value: relays?.[0] ? ["p", pubkey, relays[0]] : ["p", pubkey]}
}
if (decoded.type === "naddr") { if (decoded.type === "naddr") {
const {kind, pubkey, identifier, relays} = decoded.data const {kind, pubkey, identifier, relays} = decoded.data
const coordinate = `${kind}:${pubkey}:${identifier}` const coordinate = `${kind}:${pubkey}:${identifier}`
return {value: relays?.[0] ? ["a", coordinate, relays[0]] : ["a", coordinate]} return {value: relays?.[0] ? ["a", coordinate, relays[0]] : ["a", coordinate]}
} }
return undefined
} catch { } catch {
// Not a nostr entity; fall through to external url handling. // Not a nostr entity; fall through to external url handling.
} }
return {value: ["i", trimmed]} const parsed = parse({content: trimmed})
return parsed.length === 1 && isLink(parsed[0]) ? {value: ["i", trimmed]} : undefined
} }