diff --git a/lumina/components/UploadComponent.tsx b/lumina/components/UploadComponent.tsx index bfc14bc..2bd4aee 100644 --- a/lumina/components/UploadComponent.tsx +++ b/lumina/components/UploadComponent.tsx @@ -1,143 +1,169 @@ -import { useNostr } from 'nostr-react'; -import { finalizeEvent, nip19, NostrEvent } from 'nostr-tools'; -import React, { ChangeEvent, FormEvent, useState } from 'react'; -import { Button } from './ui/button'; -import { Textarea } from './ui/textarea'; -import { bytesToHex, hexToBytes } from '@noble/hashes/utils' -import { ReloadIcon } from '@radix-ui/react-icons'; -import { Label } from './ui/label'; -import { Input } from './ui/input'; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion" +import { useNostr } from "nostr-react" +import { finalizeEvent, nip19, type NostrEvent } from "nostr-tools" +import type React from "react" +import { type ChangeEvent, type FormEvent, useState } from "react" +import { Button } from "./ui/button" +import { Textarea } from "./ui/textarea" +import { bytesToHex, hexToBytes } from "@noble/hashes/utils" +import { ReloadIcon } from "@radix-ui/react-icons" +import { Label } from "./ui/label" +import { Input } from "./ui/input" +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion" +import { encode } from "blurhash" + +async function calculateBlurhash(file: File): Promise { + return new Promise((resolve, reject) => { + const canvas = document.createElement("canvas") + const ctx = canvas.getContext("2d") + const img = new Image() + img.onload = () => { + canvas.width = 32 + canvas.height = 32 + ctx?.drawImage(img, 0, 0, 32, 32) + const imageData = ctx?.getImageData(0, 0, 32, 32) + if (imageData) { + const blurhash = encode(imageData.data, imageData.width, imageData.height, 4, 4) + resolve(blurhash) + } else { + reject(new Error("Failed to get image data")) + } + } + img.onerror = reject + img.src = URL.createObjectURL(file) + }) +} const UploadComponent: React.FC = () => { + const { publish } = useNostr() + const { createHash } = require("crypto") + const loginType = typeof window !== "undefined" ? window.localStorage.getItem("loginType") : null + const [previewUrl, setPreviewUrl] = useState("") - const { publish } = useNostr(); - const { createHash } = require('crypto'); - const loginType = typeof window !== 'undefined' ? window.localStorage.getItem('loginType') : null; - const [previewUrl, setPreviewUrl] = useState(''); - - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(false) const handleFileChange = (event: ChangeEvent) => { - const file = event.target.files?.[0]; + const file = event.target.files?.[0] if (file) { - const url = URL.createObjectURL(file); - if (url.startsWith('blob:')) { - setPreviewUrl(url); + const url = URL.createObjectURL(file) + if (url.startsWith("blob:")) { + setPreviewUrl(url) } // Optional: Bereinigung alter URLs - return () => URL.revokeObjectURL(url); + return () => URL.revokeObjectURL(url) } - }; + } async function onSubmit(event: FormEvent) { - event.preventDefault(); - setIsLoading(true); + event.preventDefault() + setIsLoading(true) - const formData = new FormData(event.currentTarget); - const desc = formData.get('description') as string; - const file = formData.get('file') as File; - let sha256 = ''; - let finalNoteContent = desc; - let finalFileUrl = ''; - console.log('File:', file); + const formData = new FormData(event.currentTarget) + const desc = formData.get("description") as string + const file = formData.get("file") as File + let sha256 = "" + let finalNoteContent = desc + let finalFileUrl = "" + console.log("File:", file) if (!desc && !file.size) { - alert('Please enter a description and/or upload a file'); - setIsLoading(false); - return; + alert("Please enter a description and/or upload a file") + setIsLoading(false) + return } // get every hashtag in desc and cut off the # symbol - let hashtags: string[] = desc.match(/#[a-zA-Z0-9]+/g) || []; + let hashtags: string[] = desc.match(/#[a-zA-Z0-9]+/g) || [] if (hashtags) { - hashtags = hashtags.map((hashtag) => hashtag.slice(1)); + hashtags = hashtags.map((hashtag) => hashtag.slice(1)) } - // If file is is preent, upload it to the media server if (file) { const readFileAsArrayBuffer = (file: File): Promise => { return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result as ArrayBuffer); - reader.onerror = () => reject(reader.error); - reader.readAsArrayBuffer(file); - }); - }; + const reader = new FileReader() + reader.onload = () => resolve(reader.result as ArrayBuffer) + reader.onerror = () => reject(reader.error) + reader.readAsArrayBuffer(file) + }) + } try { - const arrayBuffer = await readFileAsArrayBuffer(file); - const hashBuffer = createHash('sha256').update(Buffer.from(arrayBuffer)).digest(); - sha256 = hashBuffer.toString('hex'); + const arrayBuffer = await readFileAsArrayBuffer(file) + const hashBuffer = createHash("sha256").update(Buffer.from(arrayBuffer)).digest() + sha256 = hashBuffer.toString("hex") - const unixNow = () => Math.floor(Date.now() / 1000); - const newExpirationValue = () => (unixNow() + 60 * 5).toString(); + const unixNow = () => Math.floor(Date.now() / 1000) + const newExpirationValue = () => (unixNow() + 60 * 5).toString() - const pubkey = window.localStorage.getItem('pubkey'); - const createdAt = Math.floor(Date.now() / 1000); + const pubkey = window.localStorage.getItem("pubkey") + const createdAt = Math.floor(Date.now() / 1000) // Create auth event for blossom auth via nostr - let authEvent = { + const authEvent = { kind: 24242, content: desc, created_at: createdAt, tags: [ - ['t', 'upload'], - ['x', sha256], - ['expiration', newExpirationValue()], + ["t", "upload"], + ["x", sha256], + ["expiration", newExpirationValue()], ], - }; + } - console.log(authEvent); + console.log(authEvent) // Sign auth event - let authEventSigned = {}; - if (loginType === 'extension') { - authEventSigned = await window.nostr.signEvent(authEvent); - } else if (loginType === 'amber') { + let authEventSigned = {} + if (loginType === "extension") { + authEventSigned = await window.nostr.signEvent(authEvent) + } else if (loginType === "amber") { // TODO: Sign event with amber - alert('Signing with Amber is not implemented yet, sorry!'); - } else if (loginType === 'raw_nsec') { - if (typeof window !== 'undefined') { - let nsecStr = null; - nsecStr = window.localStorage.getItem('nsec'); + alert("Signing with Amber is not implemented yet, sorry!") + } else if (loginType === "raw_nsec") { + if (typeof window !== "undefined") { + let nsecStr = null + nsecStr = window.localStorage.getItem("nsec") if (nsecStr != null) { - authEventSigned = finalizeEvent(authEvent, hexToBytes(nsecStr)); + authEventSigned = finalizeEvent(authEvent, hexToBytes(nsecStr)) } } } - console.log(authEventSigned); + console.log(authEventSigned) // Actually upload the file - await fetch('https://void.cat/upload', { - method: 'PUT', + await fetch("https://void.cat/upload", { + method: "PUT", body: file, - headers: { authorization: 'Nostr ' + btoa(JSON.stringify(authEventSigned)) }, + headers: { authorization: "Nostr " + btoa(JSON.stringify(authEventSigned)) }, }).then(async (res) => { if (res.ok) { // alert('File uploaded successfully'); - let responseText = await res.text(); + const responseText = await res.text() // alert(responseText); - let responseJson = JSON.parse(responseText); - finalFileUrl = responseJson.url; + const responseJson = JSON.parse(responseText) + finalFileUrl = responseJson.url } else { - alert(await res.text()); + alert(await res.text()) } - }); + }) } catch (error) { - alert('Error: ' + error); - console.error('Error reading file:', error); + alert("Error: " + error) + console.error("Error reading file:", error) } } - let noteTags = hashtags.map((tag) => ['t', tag]); + const noteTags = hashtags.map((tag) => ["t", tag]) + + let blurhash = "" + if (file && file.type.startsWith("image/")) { + try { + blurhash = await calculateBlurhash(file) + } catch (error) { + console.error("Error calculating blurhash:", error) + } + } // If we have a file, add the file url to the note content // and also to the note tags imeta @@ -155,39 +181,48 @@ const UploadComponent: React.FC = () => { // ] // ] if (finalFileUrl) { - // convert file into image - const image = new Image(); - image.src = URL.createObjectURL(file); + const image = new Image() + image.src = URL.createObjectURL(file) + await new Promise((resolve) => { + image.onload = resolve + }) - finalNoteContent = desc; - noteTags.push(['imeta', 'url ' + finalFileUrl, 'm ' + file.type, 'x ' + sha256, 'ox ' + sha256]); + finalNoteContent = desc + noteTags.push([ + "imeta", + "url " + finalFileUrl, + "m " + file.type, + "x " + sha256, + "ox " + sha256, + "blurhash " + blurhash, + `dim ${image.width}x${image.height}`, + ]) } - const createdAt = Math.floor(Date.now() / 1000); - + const createdAt = Math.floor(Date.now() / 1000) // Create the actual note - let noteEvent = { + const noteEvent = { kind: 20, content: finalNoteContent, created_at: createdAt, tags: noteTags, - }; + } - let signedEvent: NostrEvent | null = null; + let signedEvent: NostrEvent | null = null // Sign the actual note - if (loginType === 'extension') { - signedEvent = await window.nostr.signEvent(noteEvent); - } else if (loginType === 'amber') { + if (loginType === "extension") { + signedEvent = await window.nostr.signEvent(noteEvent) + } else if (loginType === "amber") { // TODO: Sign event with amber - alert('Signing with Amber is not implemented yet, sorry!'); - } else if (loginType === 'raw_nsec') { - if (typeof window !== 'undefined') { - let nsecStr = null; - nsecStr = window.localStorage.getItem('nsec'); + alert("Signing with Amber is not implemented yet, sorry!") + } else if (loginType === "raw_nsec") { + if (typeof window !== "undefined") { + let nsecStr = null + nsecStr = window.localStorage.getItem("nsec") if (nsecStr != null) { - signedEvent = finalizeEvent(noteEvent, hexToBytes(nsecStr)); + signedEvent = finalizeEvent(noteEvent, hexToBytes(nsecStr)) } } } @@ -196,13 +231,13 @@ const UploadComponent: React.FC = () => { if (signedEvent) { console.log("final Event: ") console.log(signedEvent) - publish(signedEvent); + publish(signedEvent) } // Redirect to the note - setIsLoading(false); + setIsLoading(false) if (signedEvent != null) { - window.location.href = '/note/' + nip19.noteEncode(signedEvent.id); + window.location.href = "/note/" + nip19.noteEncode(signedEvent.id) } } @@ -210,27 +245,35 @@ const UploadComponent: React.FC = () => { <>
- + Image Upload
- +
- {previewUrl && Preview} + {previewUrl && Preview}
{isLoading ? ( - + ) : ( - + )}
- ); + ) } -export default UploadComponent; \ No newline at end of file +export default UploadComponent \ No newline at end of file