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" 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 [isLoading, setIsLoading] = useState(false); const handleFileChange = (event: ChangeEvent) => { const file = event.target.files?.[0]; if (file) { const url = URL.createObjectURL(file); setPreviewUrl(url); // Optional: Bereinigung alter URLs return () => URL.revokeObjectURL(url); } }; async function onSubmit(event: FormEvent) { 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); if (!desc && !file.size) { 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) || []; if (hashtags) { 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); }); }; try { 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 pubkey = window.localStorage.getItem('pubkey'); const createdAt = Math.floor(Date.now() / 1000); // Create auth event (NIP-98) let authEvent = { kind: 27235, content: "", created_at: createdAt, tags: [ ['u', 'https://nostr.build/api/v2/upload/files'], ['method', "POST"], ['payload', sha256], ], }; console.log(authEvent); // Sign auth event 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'); if (nsecStr != null) { authEventSigned = finalizeEvent(authEvent, hexToBytes(nsecStr)); } } } console.log(authEventSigned); let formBodyWithFile = new FormData(); formBodyWithFile.append('file', file); // Actually upload the file await fetch('https://nostr.build/api/v2/upload/files', { method: 'POST', body: formBodyWithFile, headers: { authorization: 'Nostr ' + btoa(JSON.stringify(authEventSigned)) }, }).then(async (res) => { if (res.ok) { let responseText = await res.text(); let responseJson = JSON.parse(responseText); console.log(responseJson['data']); finalFileUrl = responseJson['data'][0]['url'] } else { alert(await res.text()); } }); } catch (error) { console.error('Error reading file:', error); } } let noteTags = hashtags.map((tag) => ['t', tag]); // If we have a file, add the file url to the note content // and also to the note tags imeta // "tags": [ // [ // "imeta", // "url https://nostr.build/i/my-image.jpg", // "m image/jpeg", // "blurhash eVF$^OI:${M{o#*0-nNFxakD-?xVM}WEWB%iNKxvR-oetmo#R-aen$", // "dim 3024x4032", // "alt A scenic photo overlooking the coast of Costa Rica", // "x ", // "fallback https://nostrcheck.me/alt1.jpg", // "fallback https://void.cat/alt1.jpg" // ] // ] if (finalFileUrl) { // convert file into image const image = new Image(); image.src = URL.createObjectURL(file); finalNoteContent = finalFileUrl + ' ' + desc; noteTags.push(['imeta', 'url ' + finalFileUrl, 'm ' + file.type, 'x ' + sha256, 'ox ' + sha256]); } const createdAt = Math.floor(Date.now() / 1000); // Create the actual note let noteEvent = { kind: 1, content: finalNoteContent, created_at: createdAt, tags: noteTags, }; let signedEvent: NostrEvent | null = null; // Sign the actual note 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'); if (nsecStr != null) { signedEvent = finalizeEvent(noteEvent, hexToBytes(nsecStr)); } } } // If the got a signed event, publish it to nostr if (signedEvent) { console.log("final Event: ") console.log(signedEvent) publish(signedEvent); } // Redirect to the note setIsLoading(false); if (signedEvent != null) { window.location.href = '/note/' + nip19.noteEncode(signedEvent.id); } } return ( <>
Image Upload
{previewUrl && Preview}
{isLoading ? ( ) : ( )}
); } export default UploadComponent;