Use kind 15 to send images in DMs

This commit is contained in:
Jon Staab
2025-06-03 16:46:30 -07:00
parent 55efb3fdfd
commit 43b207c4dc
5 changed files with 88 additions and 16 deletions
+69 -11
View File
@@ -1,9 +1,28 @@
<script lang="ts"> <script lang="ts">
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {onMount} from "svelte" import {onMount} from "svelte"
import {int, nthNe, MINUTE, sortBy, remove, formatTimestampAsDate} from "@welshman/lib" import {
import type {TrustedEvent, EventContent} from "@welshman/util" int,
import {createEvent, DIRECT_MESSAGE, INBOX_RELAYS} from "@welshman/util" ms,
partition,
spec,
nthEq,
nthNe,
MINUTE,
sortBy,
remove,
formatTimestampAsDate,
} from "@welshman/lib"
import type {TrustedEvent, EventTemplate, EventContent} from "@welshman/util"
import {parse, isLink} from "@welshman/content"
import {
createEvent,
tagsFromIMeta,
getTags,
DIRECT_MESSAGE,
DIRECT_MESSAGE_FILE,
INBOX_RELAYS,
} from "@welshman/util"
import { import {
pubkey, pubkey,
tagPubkey, tagPubkey,
@@ -61,14 +80,53 @@
} }
const onSubmit = async (params: EventContent) => { const onSubmit = async (params: EventContent) => {
// Remove p tags since they result in forking the conversation const ptags = remove($pubkey!, pubkeys).map(tagPubkey)
const tags = [...params.tags.filter(nthNe(0, "p")), ...remove($pubkey!, pubkeys).map(tagPubkey)]
await sendWrapped({ // Remove p tags since they result in forking the conversation
pubkeys, params.tags = params.tags.filter(nthNe(0, "p"))
template: createEvent(DIRECT_MESSAGE, prependParent(parent, {...params, tags})),
delay: $userSettingValues.send_delay, // Add our reply quote to content
}) params = prependParent(parent, params)
const [imetaTags, tags] = partition(nthEq(0, "imeta"), params.tags)
const imetas = getTags("imeta", imetaTags).map(tagsFromIMeta)
const templates: EventTemplate[] = []
const buffer = []
const addTemplate = (kind: number, content: string, tags: string[][]) => {
content = content.trim()
if (content) {
templates.push(createEvent(kind, {content, tags: [...tags, ...ptags]}))
}
}
for (const p of parse(params)) {
const imeta = isLink(p)
? imetas.find(tags => tags.find(spec(["url", p.value.url.toString()])))
: undefined
if (isLink(p) && imeta) {
addTemplate(DIRECT_MESSAGE, buffer.splice(0).join(""), tags)
addTemplate(
DIRECT_MESSAGE_FILE,
p.value.url.toString(),
imeta.slice(1).filter(nthNe(0, "url")),
)
} else {
buffer.push(p.raw)
}
}
addTemplate(DIRECT_MESSAGE, buffer.splice(0).join(""), tags)
// Split the message into multiple pieces so that we can use kind 15 to send images per nip 17
// Sleep 1 second between each one to make sure timestamps are distinct
for (let i = 0; i < templates.length; i++) {
const template = templates[i]
await sendWrapped({pubkeys, template, delay: $userSettingValues.send_delay + ms(i)})
}
clearParent() clearParent()
} }
@@ -191,7 +249,7 @@
{/snippet} {/snippet}
</PageBar> </PageBar>
<PageContent class="flex flex-col-reverse pt-4"> <PageContent class="flex flex-col-reverse gap-2 pt-4">
<div bind:this={dynamicPadding}></div> <div bind:this={dynamicPadding}></div>
{#if missingInboxes.includes($pubkey!)} {#if missingInboxes.includes($pubkey!)}
<div class="py-12"> <div class="py-12">
@@ -10,7 +10,7 @@
const meta = const meta =
getTags("imeta", event.tags) getTags("imeta", event.tags)
.map(tagsFromIMeta) .map(tagsFromIMeta)
.find(meta => getTagValue("url", meta) === url) || [] .find(meta => getTagValue("url", meta) === url) || event.tags
const key = getTagValue("decryption-key", meta) const key = getTagValue("decryption-key", meta)
const nonce = getTagValue("decryption-nonce", meta) const nonce = getTagValue("decryption-nonce", meta)
+10 -2
View File
@@ -5,7 +5,15 @@
import {Router} from "@welshman/router" import {Router} from "@welshman/router"
import {tracker, repository} from "@welshman/app" import {tracker, repository} from "@welshman/app"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {Address, getTagValue, DIRECT_MESSAGE, MESSAGE, THREAD, EVENT_TIME} from "@welshman/util" import {
Address,
getTagValue,
DIRECT_MESSAGE,
DIRECT_MESSAGE_FILE,
MESSAGE,
THREAD,
EVENT_TIME,
} from "@welshman/util"
import {scrollToEvent} from "@lib/html" import {scrollToEvent} from "@lib/html"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
@@ -52,7 +60,7 @@
const onclick = () => { const onclick = () => {
if ($quote) { if ($quote) {
if ($quote.kind === DIRECT_MESSAGE) { if ($quote.kind === DIRECT_MESSAGE || $quote.kind === DIRECT_MESSAGE_FILE) {
return scrollToEvent($quote.id) return scrollToEvent($quote.id)
} }
+4 -1
View File
@@ -29,6 +29,7 @@ import {
REACTION, REACTION,
ZAP_RESPONSE, ZAP_RESPONSE,
DIRECT_MESSAGE, DIRECT_MESSAGE,
DIRECT_MESSAGE_FILE,
GROUP_META, GROUP_META,
MESSAGE, MESSAGE,
GROUPS, GROUPS,
@@ -413,7 +414,9 @@ export const {
// Chats // Chats
export const chatMessages = deriveEvents(repository, {filters: [{kinds: [DIRECT_MESSAGE]}]}) export const chatMessages = deriveEvents(repository, {
filters: [{kinds: [DIRECT_MESSAGE, DIRECT_MESSAGE_FILE]}],
})
export type Chat = { export type Chat = {
id: string id: string
+4 -1
View File
@@ -17,6 +17,7 @@
MESSAGE, MESSAGE,
INBOX_RELAYS, INBOX_RELAYS,
DIRECT_MESSAGE, DIRECT_MESSAGE,
DIRECT_MESSAGE_FILE,
MUTES, MUTES,
FOLLOWS, FOLLOWS,
PROFILE, PROFILE,
@@ -177,7 +178,9 @@
return 1 return 1
} }
if ([EVENT_TIME, THREAD, MESSAGE, DIRECT_MESSAGE].includes(e.kind)) { if (
[EVENT_TIME, THREAD, MESSAGE, DIRECT_MESSAGE, DIRECT_MESSAGE_FILE].includes(e.kind)
) {
return 0.9 return 0.9
} }