mirror of
https://github.com/lumina-rocks/lumina.git
synced 2026-04-09 06:56:57 +02:00
236 lines
7.7 KiB
TypeScript
236 lines
7.7 KiB
TypeScript
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<HTMLInputElement>) => {
|
|
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<HTMLFormElement>) {
|
|
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<ArrayBuffer> => {
|
|
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 <sha256 hash as specified in NIP 94>",
|
|
// "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 (
|
|
<>
|
|
<div>
|
|
<form className="space-y-4" onSubmit={onSubmit}>
|
|
<Textarea name="description" rows={6} placeholder="Your description" id="description" className="w-full"></Textarea>
|
|
<Accordion type="single" collapsible>
|
|
<AccordionItem value="item-1">
|
|
<AccordionTrigger>Image Upload</AccordionTrigger>
|
|
<AccordionContent>
|
|
<div className="grid w-full max-w-sm items-center gap-1.5">
|
|
<Input id="file" name='file' type="file" accept='image/*' onChange={handleFileChange} />
|
|
</div>
|
|
{previewUrl && <img src={previewUrl} alt="Preview" className="w-full pt-4" />}
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
</Accordion>
|
|
{isLoading ? (
|
|
<Button className='w-full' disabled>Uploading.. <ReloadIcon className="m-2 h-4 w-4 animate-spin" /></Button>
|
|
) : (
|
|
<Button className='w-full'>Upload</Button>
|
|
)}
|
|
</form>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default UploadComponent; |