Fix editing featured content, add menu to boards and ability to share boards/pins to chat
This commit is contained in:
parent
269630efc2
commit
1581d909f0
19 changed files with 319 additions and 107 deletions
44
src/app/components/BoardMenu.svelte
Normal file
44
src/app/components/BoardMenu.svelte
Normal 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>
|
||||
|
|
@ -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})
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
16
src/app/components/NoteContentBoard.svelte
Normal file
16
src/app/components/NoteContentBoard.svelte
Normal 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>
|
||||
50
src/app/components/NoteContentPin.svelte
Normal file
50
src/app/components/NoteContentPin.svelte
Normal 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>
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
import {goToEvent} from "@app/routes"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
url?: string
|
||||
value: string
|
||||
relays: string[]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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
20
src/app/share.ts
Normal 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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Reference in a new issue