From ad0b4009b2b2ae9173fffb41f0cfa7b35edcb303 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 7 Mar 2026 08:54:47 -0800 Subject: image upload --- src/lib/messaging.ts | 1 + src/lib/mls-relay.ts | 60 ++++++++++++++++++++++ src/routes/(app)/messages/[id]/+page.svelte | 80 +++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+) diff --git a/src/lib/messaging.ts b/src/lib/messaging.ts index ba2d21ad..4cebd2b0 100644 --- a/src/lib/messaging.ts +++ b/src/lib/messaging.ts @@ -12,6 +12,7 @@ export { getGroups, getGroupRumours, sendGroupMessage, + sendGroupMediaMessage, findOrCreateDmGroup, subscribeToMessages, fetchGroupHistory, diff --git a/src/lib/mls-relay.ts b/src/lib/mls-relay.ts index 5eb8dbe3..d29cd535 100644 --- a/src/lib/mls-relay.ts +++ b/src/lib/mls-relay.ts @@ -10,8 +10,12 @@ import { SimplePool } from "nostr-tools/pool"; import { Relay } from "nostr-tools/relay"; import { getMarmotClient, getInviteReader, DM_RELAYS } from "$lib/marmot"; import { parseMediaImetaTag } from "@internet-privacy/marmot-ts"; +import { BlossomClient } from "nostr-tools/nipb7"; +import { ensureSigner } from "$lib/nip07"; import { post } from "$lib/utils"; +const BLOSSOM_SERVER = "https://blossom.primal.net"; + const pool = new SimplePool(); /** Extract member pubkeys from an MLS group's ratchet tree (pass group.state). */ @@ -412,6 +416,62 @@ export async function sendGroupMessage( cacheGroupRumour(groupId, rumour); } +/** Send a media message (image/video/audio) to a group. */ +export async function sendGroupMediaMessage( + user: any, + groupId: string, + file: File | Blob, +): Promise { + const client = getMarmotClient(user.pubkey); + const group = await client.getGroup(groupId); + + const mime = file.type || "application/octet-stream"; + const filename = file instanceof File ? file.name : "file"; + + // Encrypt with MLS group key + const { encrypted, attachment } = await group.encryptMedia(file, { + type: mime, + filename, + }); + + // Upload encrypted blob to blossom + const sk = await ensureSigner(user.pubkey); + const signer = sk + ? { + getPublicKey: () => user.pubkey, + signEvent: async (draft: any) => { + const { finalizeEvent } = await import("nostr-tools/pure"); + return finalizeEvent(draft, sk); + }, + } + : (window as any).nostr; + + const blossom = new BlossomClient(BLOSSOM_SERVER, signer); + const encryptedBlob = new Blob([encrypted as BlobPart], { type: "application/octet-stream" }); + const desc = await blossom.uploadBlob(encryptedBlob); + + // Build imeta tag with the upload URL and MIP-04 fields + const imetaTag = [ + "imeta", + `url ${desc.url}`, + `m ${attachment.type}`, + `x ${attachment.sha256}`, + `filename ${attachment.filename}`, + `n ${attachment.nonce}`, + `v ${attachment.version}`, + ]; + if (attachment.size) imetaTag.push(`size ${attachment.size}`); + + // Pre-cache plaintext so the image renders instantly for us + await putCachedMedia(attachment.sha256, file, mime); + + // Send as chat message with the imeta tag + await group.sendChatMessage("", [imetaTag]); + + const rumour = makeRumour("", user.pubkey, groupId, [imetaTag]); + cacheGroupRumour(groupId, rumour); +} + // --------------------------------------------------------------------------- // Dynamic group subscription // --------------------------------------------------------------------------- diff --git a/src/routes/(app)/messages/[id]/+page.svelte b/src/routes/(app)/messages/[id]/+page.svelte index f9aea4da..a5768df7 100644 --- a/src/routes/(app)/messages/[id]/+page.svelte +++ b/src/routes/(app)/messages/[id]/+page.svelte @@ -1,11 +1,13 @@