Route and protect space content through toSpace and publishToSpace instead of repeating the NIP-70 and room checks in every form

This commit is contained in:
Jon Staab 2026-09-25 15:55:39 -07:00
parent d67385632b
commit ba03406795
14 changed files with 109 additions and 193 deletions

View file

@ -2,8 +2,6 @@
import type {Snippet} from "svelte"
import {writable} from "svelte/store"
import {randomId} from "@welshman/lib"
import {relay} from "@welshman/util"
import {publish} from "@welshman/app"
import {TimeEvent} from "@welshman/domain"
import {preventDefault} from "@lib/html"
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
@ -18,11 +16,11 @@
import ModalBody from "@lib/components/ModalBody.svelte"
import DateTimeRangeInput from "@lib/components/DateTimeRangeInput.svelte"
import EditorContent from "@app/editor/EditorContent.svelte"
import {publishRoomQuote} from "@app/rooms"
import {publishToSpace} from "@app/rooms"
import {makeEditor} from "@app/editor"
import {DraftKey} from "@app/drafts"
import {pushToast} from "@app/toast"
import {command, relays, writer} from "@app/core"
import {writer} from "@app/core"
type Values = {
d: string
@ -61,8 +59,6 @@
initialValues = draftKey.get()
}
const shouldProtect = $relays.hasNip(url, 70)
const uploading = writable(false)
const back = () => history.back()
@ -101,7 +97,6 @@
loading = true
try {
const protect = await shouldProtect
const eventWriter = writer(TimeEvent)
.setIdentifier(d)
.setTitle(title)
@ -109,17 +104,9 @@
.setStart(start)
.setEnd(end)
.setContent(content)
.setProtected(protect)
.addTags(...ed.storage.nostr.getEditorTags())
if (h) {
eventWriter.setRoom(url, h)
} else {
eventWriter.forceRoutes(relay(url))
}
const calendarThunk = await command(eventWriter).then(publish)
const error = await calendarThunk.waitForError()
const {error} = await publishToSpace(eventWriter, {url, h, shareToChat})
if (error) {
return pushToast({theme: "error", message: error})
@ -127,11 +114,6 @@
draftKey?.clear()
history.back()
if (shareToChat) {
publishRoomQuote({url, h, parent: calendarThunk.options.event, protect})
}
pushToast({message: "Your event has been saved!"})
} finally {
loading = false

View file

@ -2,8 +2,6 @@
import type {Snippet} from "svelte"
import cx from "classnames"
import {removeUndefined, randomId, uniq, toggle} from "@welshman/lib"
import {relay} from "@welshman/util"
import {publish} from "@welshman/app"
import {Classified} from "@welshman/domain"
import {isMobile, preventDefault} from "@lib/html"
import {normalizeTopic} from "@lib/util"
@ -20,9 +18,9 @@
import CurrencyInput from "@app/components/CurrencyInput.svelte"
import TopicMultiSelect from "@app/components/TopicMultiSelect.svelte"
import EditorContent from "@app/editor/EditorContent.svelte"
import {command, relays, writer} from "@app/core"
import {writer} from "@app/core"
import {pushToast} from "@app/toast"
import {publishRoomQuote} from "@app/rooms"
import {publishToSpace} from "@app/rooms"
import {makeEditor} from "@app/editor"
import {DraftKey} from "@app/drafts"
import {compressFileForUpload, uploadFile} from "@app/uploads"
@ -100,8 +98,6 @@
}
}
const protect = await $relays.hasNip(url, 70)
const eventWriter = writer(Classified)
.setIdentifier(d)
.setContent(content)
@ -111,17 +107,9 @@
.setStatus(status)
.setTopics(topics)
.setImages(imageUrls)
.setProtected(protect)
.addTags(...ed.storage.nostr.getEditorTags())
if (h) {
eventWriter.setRoom(url, h)
} else {
eventWriter.forceRoutes(relay(url))
}
const classifiedThunk = await command(eventWriter).then(publish)
const error = await classifiedThunk.waitForError()
const {error} = await publishToSpace(eventWriter, {url, h, shareToChat})
if (error) {
return pushToast({theme: "error", message: error})
@ -129,10 +117,6 @@
draftKey?.clear()
history.back()
if (shareToChat) {
publishRoomQuote({url, h, parent: classifiedThunk.options.event, protect})
}
} finally {
loading = false
}

View file

@ -9,9 +9,10 @@
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import EditorContent from "@app/editor/EditorContent.svelte"
import {relays, thunks, writer} from "@app/core"
import {thunks, writer} from "@app/core"
import {DraftKey} from "@app/drafts"
import {makeEditor} from "@app/editor"
import {toSpace} from "@app/rooms"
import {loadSendDelay} from "@app/settings"
import {pushToast} from "@app/toast"
@ -41,17 +42,15 @@
// A reply to a room event is a NIP-22 comment, and a reply to a kind 1 note is a NIP-10 note.
const renderReply = async (content: string, tags: string[][]) => {
if (url) {
const eventWriter = writer(Comment)
.setContent(content)
.addTags(...tags)
.setRootFromEvent(event)
.setParentFromEvent(parent ?? event)
.setProtected(await $relays.hasNip(url, 70))
// An untagged comment isn't visible to the group, so the relay neither gates nor deletes it with the room.
if (h) {
eventWriter.setRoom(url, h)
}
const eventWriter = await toSpace(
writer(Comment)
.setContent(content)
.addTags(...tags)
.setRootFromEvent(event)
.setParentFromEvent(parent ?? event),
url,
h,
)
return {relays: [url], event: await eventWriter.renderTemplate()}
}

View file

@ -12,11 +12,11 @@
import Spinner from "@lib/components/Spinner.svelte"
import EditorContent from "@app/editor/EditorContent.svelte"
import ComposeParent from "@app/components/ComposeParent.svelte"
import {prependParent} from "@app/rooms"
import {prependParent, toSpace} from "@app/rooms"
import {makeEditor} from "@app/editor"
import {DraftKey} from "@app/drafts"
import {pushToast} from "@app/toast"
import {relays, thunks, writer} from "@app/core"
import {thunks, writer} from "@app/core"
import {loadSendDelay} from "@app/settings"
type Values = {
@ -61,17 +61,15 @@
;({content, tags} = await prependParent(parent, {content, tags}, url))
}
const eventWriter = writer(Comment)
.setContent(content)
.addTags(...tags)
.setRootFromEvent(event)
.setParentFromEvent(event)
.setProtected(await $relays.hasNip(url, 70))
// An untagged comment isn't visible to the group, so the relay neither gates nor deletes it with the room.
if (h) {
eventWriter.setRoom(url, h)
}
const eventWriter = await toSpace(
writer(Comment)
.setContent(content)
.addTags(...tags)
.setRootFromEvent(event)
.setParentFromEvent(event),
url,
h,
)
const thunk = $thunks.publish({
relays: [url],

View file

@ -1,9 +1,8 @@
<script lang="ts">
import {writable} from "svelte/store"
import {now} from "@welshman/lib"
import {relay, toMsats} from "@welshman/util"
import {toMsats} from "@welshman/util"
import {ZapGoal} from "@welshman/domain"
import {publish} from "@welshman/app"
import {isMobile, preventDefault} from "@lib/html"
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
import Bolt from "@assets/icons/bolt.svg?dataurl"
@ -22,9 +21,9 @@
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import EditorContent from "@app/editor/EditorContent.svelte"
import {command, relays, writer} from "@app/core"
import {writer} from "@app/core"
import {pushToast} from "@app/toast"
import {publishRoomQuote} from "@app/rooms"
import {publishToSpace} from "@app/rooms"
import {makeEditor} from "@app/editor"
import {DraftKey} from "@app/drafts"
import {compressFileForUpload, uploadFile} from "@app/uploads"
@ -49,8 +48,6 @@
const initialValues = draftKey.get()
const shouldProtect = $relays.hasNip(url, 70)
const uploading = writable(false)
const back = () => history.back()
@ -103,15 +100,12 @@
imageUrl = result?.url
}
const protect = await shouldProtect
const eventWriter = writer(ZapGoal)
.setTitle(title)
.setSummary(content)
.setAmount(toMsats(amount))
.setUrls([url])
.setProtected(protect)
.addTags(...ed.storage.nostr.getEditorTags())
.forceRoutes(relay(url))
if (imageUrl) {
eventWriter.setImage(imageUrl)
@ -121,12 +115,7 @@
eventWriter.setClosedAt(closedAt)
}
if (h) {
eventWriter.setRoom(url, h)
}
const goalThunk = await command(eventWriter).then(publish)
const error = await goalThunk.waitForError()
const {error} = await publishToSpace(eventWriter, {url, h, shareToChat})
if (error) {
return pushToast({theme: "error", message: error})
@ -134,10 +123,6 @@
draftKey.clear()
history.back()
if (shareToChat) {
publishRoomQuote({url, h, parent: goalThunk.options.event, protect})
}
} finally {
loading = false
}

View file

@ -1,10 +1,10 @@
<script lang="ts">
import {relay} from "@welshman/util"
import {Pin} from "@welshman/domain"
import {publish} from "@welshman/app"
import PinForm, {type PinFormValues} from "@app/components/PinForm.svelte"
import {command, relays, writer} from "@app/core"
import {command, writer} from "@app/core"
import {setPinReference} from "@app/pinboards"
import {toSpace} from "@app/rooms"
type Props = {
url: string
@ -15,11 +15,7 @@
const {url, address, reference}: Props = $props()
const submit = async ({title, topics, value, content}: PinFormValues) => {
const eventWriter = writer(Pin)
.setIdentifier()
.addBoard(address)
.setProtected(await $relays.hasNip(url, 70))
.forceRoutes(relay(url))
const eventWriter = await toSpace(writer(Pin).setIdentifier().addBoard(address), url)
if (!setPinReference(eventWriter, value)) {
return "Please enter a valid URL or nostr link."

View file

@ -1,11 +1,11 @@
<script lang="ts">
import {relay} from "@welshman/util"
import {Pin} from "@welshman/domain"
import {publish} from "@welshman/app"
import type {PinReader} from "@welshman/domain"
import PinForm, {type PinFormValues} from "@app/components/PinForm.svelte"
import {command, relays, writer} from "@app/core"
import {command, writer} from "@app/core"
import {pinToReference, setPinReference} from "@app/pinboards"
import {toSpace} from "@app/rooms"
type Props = {
url: string
@ -15,9 +15,7 @@
const {url, pin}: Props = $props()
const submit = async ({title, topics, value, content}: PinFormValues) => {
const eventWriter = writer(Pin, pin)
.setProtected(await $relays.hasNip(url, 70))
.forceRoutes(relay(url))
const eventWriter = await toSpace(writer(Pin, pin), url)
if (!setPinReference(eventWriter, value)) {
return "Please enter a valid URL or nostr link."

View file

@ -1,6 +1,6 @@
<script lang="ts">
import {navigate} from "@app/modal"
import {getAddress, relay} from "@welshman/util"
import {getAddress} from "@welshman/util"
import {publish} from "@welshman/app"
import {Pinboard} from "@welshman/domain"
import type {PinboardReader} from "@welshman/domain"
@ -16,7 +16,8 @@
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import RelayName from "@app/components/RelayName.svelte"
import {command, relays, writer} from "@app/core"
import {command, writer} from "@app/core"
import {toSpace} from "@app/rooms"
import {makeLibraryPath} from "@app/routes"
import {pushToast} from "@app/toast"
@ -33,11 +34,10 @@
loading = true
try {
const eventWriter = writer(Pinboard, board)
.setTitle(title)
.setDescription(description)
.setProtected(await $relays.hasNip(url, 70))
.forceRoutes(relay(url))
const eventWriter = await toSpace(
writer(Pinboard, board).setTitle(title).setDescription(description),
url,
)
if (!board) {
eventWriter.setIdentifier().setCollaborative(true)

View file

@ -1,7 +1,5 @@
<script lang="ts">
import {insertAt, now, randomId, removeAt, removeUndefined, spec} from "@welshman/lib"
import {relay} from "@welshman/util"
import {publish} from "@welshman/app"
import {Poll} from "@welshman/domain"
import type {PollType} from "@welshman/domain"
import {isMobile, preventDefault} from "@lib/html"
@ -21,9 +19,9 @@
import ModalFooter from "@lib/components/ModalFooter.svelte"
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import {command, relays, writer} from "@app/core"
import {writer} from "@app/core"
import {pushToast} from "@app/toast"
import {publishRoomQuote} from "@app/rooms"
import {publishToSpace} from "@app/rooms"
import {DraftKey} from "@app/drafts"
type Option = {
@ -123,12 +121,7 @@
loading = true
try {
const protect = await $relays.hasNip(url, 70)
const eventWriter = writer(Poll)
.setTitle(title.trim())
.setPollType(pollType)
.setUrls([url])
.setProtected(protect)
const eventWriter = writer(Poll).setTitle(title.trim()).setPollType(pollType).setUrls([url])
for (const option of nonEmptyOptions) {
eventWriter.addOption(option)
@ -138,14 +131,7 @@
eventWriter.setEndsAt(endsAt)
}
if (h) {
eventWriter.setRoom(url, h)
} else {
eventWriter.forceRoutes(relay(url))
}
const pollThunk = await command(eventWriter).then(publish)
const error = await pollThunk.waitForError()
const {error} = await publishToSpace(eventWriter, {url, h, shareToChat})
if (error) {
return pushToast({theme: "error", message: error})
@ -153,10 +139,6 @@
draftKey.clear()
history.back()
if (shareToChat) {
publishRoomQuote({url, h, parent: pollThunk.options.event, protect})
}
} finally {
loading = false
}

View file

@ -9,7 +9,7 @@
import {now, ifLet, ago, spec, MINUTE} from "@welshman/lib"
import type {Maybe} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util"
import {relay, stamp, MESSAGE, RELAY_ADD_MEMBER, ROOM_ADD_MEMBER} from "@welshman/util"
import {stamp, MESSAGE, RELAY_ADD_MEMBER, ROOM_ADD_MEMBER} from "@welshman/util"
import {Message} from "@welshman/domain"
import {MembershipStatus} from "@welshman/app"
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
@ -42,6 +42,7 @@
getRoomType,
groupRoomMessages,
prependParent,
toSpace,
} from "@app/rooms"
import {loadSendDelay} from "@app/settings"
import {isFeedLoading, makeFeed, makeFeedContext, makeScrollLoader} from "@app/feeds"
@ -174,14 +175,14 @@
return
}
const protect = await shouldProtect
if (eventToEdit) {
// Don't do anything if message hasn't changed
if (eventToEdit.content === content) {
return clearEventToEdit()
}
const protect = await shouldProtect
// Delete the previous message, to be republished below with the same timestamp
const command = await $deletes.deleteEvent($state.snapshot(eventToEdit), w =>
w.setProtected(protect),
@ -195,16 +196,13 @@
;({content, tags} = await prependParent(sharedEvent, {content, tags}, url))
}
const eventWriter = writer(Message)
.setContent(content)
.addTags(...tags)
.setProtected(protect)
if (h) {
eventWriter.setRoom(url, h)
} else {
eventWriter.forceRoutes(relay(url))
}
const eventWriter = await toSpace(
writer(Message)
.setContent(content)
.addTags(...tags),
url,
h,
)
if (parent) {
eventWriter.setParent(parent)

View file

@ -1,8 +1,6 @@
<script lang="ts">
import {writable} from "svelte/store"
import {relay} from "@welshman/util"
import {Thread} from "@welshman/domain"
import {publish} from "@welshman/app"
import {isMobile, preventDefault} from "@lib/html"
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@ -17,10 +15,10 @@
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import EditorContent from "@app/editor/EditorContent.svelte"
import {command, relays, writer} from "@app/core"
import {writer} from "@app/core"
import {DraftKey} from "@app/drafts"
import {makeEditor} from "@app/editor"
import {displayRoom, publishRoomQuote} from "@app/rooms"
import {displayRoom, publishToSpace} from "@app/rooms"
import {pushToast} from "@app/toast"
type Values = {
@ -39,7 +37,6 @@
const {url, h, shareToChat = false, initialValues}: Props = $props()
const draftKey = new DraftKey<Values>(`thread:${url}:${h ?? ""}`)
const draft = draftKey.get()
const shouldProtect = $relays.hasNip(url, 70)
const room = h ?? initialValues?.h ?? draft?.h ?? ""
const uploading = writable(false)
@ -73,20 +70,12 @@
loading = true
try {
const protect = await shouldProtect
const eventWriter = writer(Thread)
.setContent(content)
.setTitle(title)
.setProtected(protect)
.addTags(...ed.storage.nostr.getEditorTags())
.forceRoutes(relay(url))
if (room) {
eventWriter.setRoom(url, room)
}
const thunk = await command(eventWriter).then(publish)
const error = await thunk.waitForError()
const {error} = await publishToSpace(eventWriter, {url, h: room, shareToChat})
if (error) {
return pushToast({theme: "error", message: error})
@ -94,10 +83,6 @@
draftKey.clear()
history.back()
if (shareToChat) {
publishRoomQuote({url, h: room, parent: thunk.options.event, protect})
}
} finally {
loading = false
}

View file

@ -1,9 +1,10 @@
import {derived} from "svelte/store"
import {APP_DATA, relay, sortEventsDesc, tagSpec, tagValues} from "@welshman/util"
import {APP_DATA, sortEventsDesc, tagSpec, tagValues} from "@welshman/util"
import {AppData} from "@welshman/domain"
import {Relays, publish} from "@welshman/app"
import {command, fromApp, relays, writer} from "@app/core"
import {command, fromApp, writer} from "@app/core"
import {deriveEventsForUrl} from "@app/repository"
import {toSpace} from "@app/rooms"
// NIP-78 app data from the pubkey the space's NIP-11 names, one ["content", <value>] tag per entry.
export const FEATURED_CONTENT_D = "flotilla/featured-content"
@ -29,9 +30,7 @@ export const setFeaturedContent = async (url: string, content: string[]) => {
const eventWriter = writer(AppData)
.setIdentifier(FEATURED_CONTENT_D)
.setProtected(await relays.get().hasNip(url, 70))
.forceRoutes(relay(url))
.addTags(...tags)
return command(eventWriter).then(publish)
return command(await toSpace(eventWriter, url)).then(publish)
}

View file

@ -15,10 +15,12 @@ import {
} from "@welshman/util"
import type {EventContent, TrustedEvent} from "@welshman/util"
import {Message} from "@welshman/domain"
import type {BaseEventReader, EventWriter} from "@welshman/domain"
import {MembershipStatus, RoomLists, makeRoomKey, createSearch, publish} from "@welshman/app"
import type {Room, RoomMeta} from "@welshman/app"
import {
deriveUserItem,
command,
relayManagement,
relayMemberLists,
roomLists,
@ -101,20 +103,12 @@ export const prependParent = async (
return {content, tags}
}
export const publishRoomQuote = async ({
url,
h,
parent,
protect,
delay,
}: {
url: string
h?: string
parent: TrustedEvent
protect: boolean
delay?: number
}) => {
const eventWriter = writer(Message).setParent(parent).setProtected(protect)
export const toSpace = async <W extends EventWriter<BaseEventReader>>(
eventWriter: W,
url: string,
h?: string,
) => {
eventWriter.setProtected(await relays.get().hasNip(url, 70))
if (h) {
eventWriter.setRoom(url, h)
@ -122,6 +116,22 @@ export const publishRoomQuote = async ({
eventWriter.forceRoutes(relay(url))
}
return eventWriter
}
export const publishRoomQuote = async ({
url,
h,
parent,
delay,
}: {
url: string
h?: string
parent: TrustedEvent
delay?: number
}) => {
const eventWriter = await toSpace(writer(Message).setParent(parent), url, h)
return thunks.get().publish({
relays: [url],
event: await eventWriter.renderTemplate(),
@ -129,6 +139,20 @@ export const publishRoomQuote = async ({
})
}
export const publishToSpace = async (
eventWriter: EventWriter<BaseEventReader>,
{url, h, shareToChat}: {url: string; h?: string; shareToChat?: boolean},
) => {
const thunk = await command(await toSpace(eventWriter, url, h)).then(publish)
const error = await thunk.waitForError()
if (shareToChat && !error) {
publishRoomQuote({url, h, parent: thunk.options.event})
}
return {thunk, error}
}
// User
export const userRoomList = deriveUserItem(RoomLists)

View file

@ -4,9 +4,8 @@
import {page} from "$app/stores"
import {goto} from "$app/navigation"
import {now, randomId} from "@welshman/lib"
import {getAddress, relay} from "@welshman/util"
import {getAddress} from "@welshman/util"
import {Article} from "@welshman/domain"
import {publish} from "@welshman/app"
import {isMobile} from "@lib/html"
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
import DocumentText from "@assets/icons/document-text.svg?dataurl"
@ -20,11 +19,11 @@
import SpaceBar from "@app/components/SpaceBar.svelte"
import TopicMultiSelect from "@app/components/TopicMultiSelect.svelte"
import EditorContent from "@app/editor/EditorContent.svelte"
import {command, relays, writer} from "@app/core"
import {writer} from "@app/core"
import {DraftKey} from "@app/drafts"
import {makeEditor} from "@app/editor"
import {decodeRelay} from "@app/relays"
import {publishRoomQuote} from "@app/rooms"
import {publishToSpace} from "@app/rooms"
import {makeArticlePath} from "@app/routes"
import {compressFileForUpload, uploadFile} from "@app/uploads"
import {pushToast} from "@app/toast"
@ -48,7 +47,6 @@
const draftKey = new DraftKey<Values>(`article:${url}:${h ?? ""}`)
const initialValues = draftKey.get()
const shouldProtect = $relays.hasNip(url, 70)
const coverInputId = randomId()
const uploading = writable(false)
@ -114,16 +112,13 @@
loading = true
try {
const protect = await shouldProtect
const eventWriter = writer(Article)
.setIdentifier(d)
.setContent(content)
.setTitle(title)
.setTopics(topics)
.setPublishedAt(now())
.setProtected(protect)
.addTags(...ed.storage.nostr.getEditorTags())
.forceRoutes(relay(url))
if (summary) {
eventWriter.setSummary(summary)
@ -133,12 +128,7 @@
eventWriter.setImage(image)
}
if (h) {
eventWriter.setRoom(url, h)
}
const thunk = await command(eventWriter).then(publish)
const error = await thunk.waitForError()
const {thunk, error} = await publishToSpace(eventWriter, {url, h, shareToChat})
if (error) {
return pushToast({theme: "error", message: error})
@ -146,10 +136,6 @@
draftKey.clear()
goto(makeArticlePath(url, getAddress(thunk.options.event)))
if (shareToChat) {
publishRoomQuote({url, h, parent: thunk.options.event, protect})
}
} finally {
loading = false
}