mirror of
https://github.com/lumina-rocks/lumina.git
synced 2026-04-09 15:06:56 +02:00
Feature: Post & File Upload (#31)
* upload images to void.cat when using UploadComponent * enable upload in bottom bar * rm link in content (used to be like this for kind 1 but not for kind 20) * add blurhash and other tags + refactor UploadComponent * add blurhash dependency * move Global next to Follower Feed in Bottom Bar * refactor UploadComponent to extract signEvent function for better readability and maintainability * check uploaded note by fetching it. showing this in a drawer for now * return null when logged in with amber web-style because its not implemented yet * move Nostr Event signing to utils * change file upload from void.cat to nostr.download
This commit is contained in:
@@ -47,16 +47,16 @@ export default function BottomBar() {
|
||||
<span className="sr-only">Follower Feed</span>
|
||||
</Link>
|
||||
)}
|
||||
{/* {pubkey && window.localStorage.getItem('loginType') != 'readOnly_npub' && (
|
||||
<Link className={`flex flex-col items-center justify-center w-full text-xs gap-1 px-4 ${isActive('/upload', pathname)}`} href="/upload">
|
||||
<UploadIcon className={`h-6 w-6`} />
|
||||
<span className="sr-only">Upload</span>
|
||||
</Link>
|
||||
)} */}
|
||||
<Link className={`flex flex-col items-center justify-center w-full text-xs gap-1 px-4 ${isActive('/global', pathname)}`} href="/global">
|
||||
<GlobeIcon className={`h-6 w-6`} />
|
||||
<span className="sr-only">Global</span>
|
||||
</Link>
|
||||
{pubkey && window.localStorage.getItem('loginType') != 'readOnly_npub' && (
|
||||
<Link className={`flex flex-col items-center justify-center w-full text-xs gap-1 px-4 ${isActive('/upload', pathname)}`} href="/upload">
|
||||
<UploadIcon className={`h-6 w-6`} />
|
||||
<span className="sr-only">Upload</span>
|
||||
</Link>
|
||||
)}
|
||||
<Link className={`flex flex-col items-center justify-center w-full text-xs gap-1 px-4 ${isActive('/search', pathname)}`} href="/search">
|
||||
<SearchIcon className={`h-6 w-6`} />
|
||||
<span className="sr-only">Search</span>
|
||||
|
||||
@@ -1,205 +1,261 @@
|
||||
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 { useNostr, useNostrEvents } from "nostr-react"
|
||||
import { finalizeEvent, nip19, type NostrEvent } from "nostr-tools"
|
||||
import type React from "react"
|
||||
import { type ChangeEvent, type FormEvent, useState, useEffect, useCallback } 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"
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion"
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
} from "@/components/ui/drawer"
|
||||
import { Spinner } from "@/components/spinner"
|
||||
import { signEvent } from "@/utils/utils"
|
||||
|
||||
async function calculateBlurhash(file: File): Promise<string> {
|
||||
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 [isDrawerOpen, setIsDrawerOpen] = useState(false)
|
||||
const [uploadedNoteId, setUploadedNoteId] = useState("")
|
||||
const [retryCount, setRetryCount] = useState(0)
|
||||
const [shouldFetch, setShouldFetch] = useState(false)
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { events, isLoading: isNoteLoading } = useNostrEvents({
|
||||
filter: shouldFetch
|
||||
? {
|
||||
ids: uploadedNoteId ? [uploadedNoteId] : [],
|
||||
kinds: [20],
|
||||
limit: 1,
|
||||
}
|
||||
: { ids: [], kinds: [20], limit: 1 },
|
||||
enabled: shouldFetch,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (uploadedNoteId) {
|
||||
setShouldFetch(true)
|
||||
}
|
||||
}, [uploadedNoteId])
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId: NodeJS.Timeout
|
||||
|
||||
if (shouldFetch && events.length === 0 && !isNoteLoading) {
|
||||
timeoutId = setTimeout(() => {
|
||||
setRetryCount((prevCount) => prevCount + 1)
|
||||
setShouldFetch(false)
|
||||
setShouldFetch(true)
|
||||
}, 5000) // Retry every 5 seconds
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
}, [shouldFetch, events, isNoteLoading])
|
||||
|
||||
const handleRetry = useCallback(() => {
|
||||
setRetryCount((prevCount) => prevCount + 1)
|
||||
setShouldFetch(false)
|
||||
setShouldFetch(true)
|
||||
}, [])
|
||||
|
||||
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
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<HTMLFormElement>) {
|
||||
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 is present, 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);
|
||||
});
|
||||
};
|
||||
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: NostrEvent = {
|
||||
kind: 24242,
|
||||
content: desc,
|
||||
created_at: createdAt,
|
||||
tags: [
|
||||
['t', 'upload'],
|
||||
['x', sha256],
|
||||
['expiration', newExpirationValue()],
|
||||
["t", "upload"],
|
||||
["x", sha256],
|
||||
["expiration", newExpirationValue()],
|
||||
],
|
||||
};
|
||||
pubkey: "", // Add a placeholder for pubkey
|
||||
id: "", // Add a placeholder for id
|
||||
sig: "", // Add a placeholder for sig
|
||||
}
|
||||
|
||||
console.log(authEvent);
|
||||
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 authEventSigned = (await signEvent(loginType, authEvent)) as NostrEvent
|
||||
|
||||
// Actually upload the file
|
||||
await fetch('https://media.lumina.rocks/upload', {
|
||||
method: 'PUT',
|
||||
await fetch("https://nostr.download/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) {
|
||||
let responseText = await res.text();
|
||||
let responseJson = JSON.parse(responseText);
|
||||
finalFileUrl = responseJson.url;
|
||||
const responseText = await res.text()
|
||||
const responseJson = JSON.parse(responseText)
|
||||
finalFileUrl = responseJson.url
|
||||
} else {
|
||||
alert(await res.text());
|
||||
alert(await res.text())
|
||||
}
|
||||
});
|
||||
})
|
||||
} catch (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])
|
||||
|
||||
// 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]);
|
||||
let blurhash = ""
|
||||
if (file && file.type.startsWith("image/")) {
|
||||
try {
|
||||
blurhash = await calculateBlurhash(file)
|
||||
} catch (error) {
|
||||
console.error("Error calculating blurhash:", error)
|
||||
}
|
||||
}
|
||||
|
||||
const createdAt = Math.floor(Date.now() / 1000);
|
||||
if (finalFileUrl) {
|
||||
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,
|
||||
"blurhash " + blurhash,
|
||||
`dim ${image.width}x${image.height}`,
|
||||
])
|
||||
}
|
||||
|
||||
const createdAt = Math.floor(Date.now() / 1000)
|
||||
|
||||
// Create the actual note
|
||||
let noteEvent = {
|
||||
kind: 1,
|
||||
const noteEvent: NostrEvent = {
|
||||
kind: 20,
|
||||
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));
|
||||
}
|
||||
}
|
||||
pubkey: "", // Add a placeholder for pubkey
|
||||
id: "", // Add a placeholder for id
|
||||
sig: "", // Add a placeholder for sig
|
||||
}
|
||||
|
||||
// If the got a signed event, publish it to nostr
|
||||
let signedEvent: NostrEvent | null = null
|
||||
|
||||
// Sign the actual note
|
||||
signedEvent = (await signEvent(loginType, noteEvent)) as NostrEvent
|
||||
|
||||
// If we got a signed event, publish it to nostr
|
||||
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);
|
||||
setUploadedNoteId(signedEvent.id)
|
||||
setIsDrawerOpen(true)
|
||||
setShouldFetch(true)
|
||||
setRetryCount(0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,27 +263,69 @@ const UploadComponent: React.FC = () => {
|
||||
<>
|
||||
<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>
|
||||
<Textarea
|
||||
name="description"
|
||||
rows={6}
|
||||
placeholder="Your description"
|
||||
id="description"
|
||||
className="w-full"
|
||||
></Textarea>
|
||||
<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 || "/placeholder.svg"} alt="Preview" className="w-full pt-4" />}
|
||||
{isLoading ? (
|
||||
<Button className='w-full' disabled>Uploading.. <ReloadIcon className="m-2 h-4 w-4 animate-spin" /></Button>
|
||||
<Button className="w-full" disabled>
|
||||
Uploading.. <ReloadIcon className="m-2 h-4 w-4 animate-spin" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button className='w-full'>Upload</Button>
|
||||
<Button className="w-full">Upload</Button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
<Drawer open={isDrawerOpen} onOpenChange={setIsDrawerOpen}>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Upload Status</DrawerTitle>
|
||||
<DrawerDescription>
|
||||
{isNoteLoading ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Spinner />
|
||||
<span>Checking note status...</span>
|
||||
</div>
|
||||
) : events.length > 0 ? (
|
||||
<div
|
||||
className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative"
|
||||
role="alert"
|
||||
>
|
||||
<strong className="font-bold">Success!</strong>
|
||||
<span className="block sm:inline"> Note found with ID: </span>
|
||||
<span className="block sm:inline font-mono">
|
||||
{`${events[0].id.slice(0, 5)}...${events[0].id.slice(-3)}`}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<p>Note not found. It may take a moment to propagate.</p>
|
||||
)}
|
||||
</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<DrawerFooter className="flex flex-col space-y-2">
|
||||
{events.length === 0 && (
|
||||
<Button onClick={handleRetry} variant="outline" className="w-full">
|
||||
Retry Now
|
||||
</Button>
|
||||
)}
|
||||
<Button asChild className="w-full">
|
||||
<a href={`/note/${nip19.noteEncode(uploadedNoteId)}`}>View Note</a>
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setIsDrawerOpen(false)} className="w-full">
|
||||
Close
|
||||
</Button>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default UploadComponent;
|
||||
export default UploadComponent
|
||||
20
lumina/components/spinner.tsx
Normal file
20
lumina/components/spinner.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export function Spinner({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={cn("animate-spin", className)}
|
||||
>
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
7
lumina/package-lock.json
generated
7
lumina/package-lock.json
generated
@@ -21,6 +21,7 @@
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"blurhash": "^2.0.5",
|
||||
"bolt11": "^1.4.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
@@ -4155,6 +4156,12 @@
|
||||
"resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz",
|
||||
"integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg=="
|
||||
},
|
||||
"node_modules/blurhash": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz",
|
||||
"integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bn.js": {
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"blurhash": "^2.0.5",
|
||||
"bolt11": "^1.4.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Event as NostrEvent } from "nostr-tools";
|
||||
import { Event as NostrEvent, finalizeEvent} from "nostr-tools";
|
||||
import { hexToBytes } from "@noble/hashes/utils"
|
||||
|
||||
export function getImageUrl(tags: string[][]): string {
|
||||
const imetaTag = tags.find(tag => tag[0] === 'imeta');
|
||||
if (imetaTag) {
|
||||
const urlItem = imetaTag.find(item => item.startsWith('url '));
|
||||
if (urlItem) {
|
||||
return urlItem.split(' ')[1];
|
||||
}
|
||||
const imetaTag = tags.find(tag => tag[0] === 'imeta');
|
||||
if (imetaTag) {
|
||||
const urlItem = imetaTag.find(item => item.startsWith('url '));
|
||||
if (urlItem) {
|
||||
return urlItem.split(' ')[1];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export function extractDimensions(event: NostrEvent): { width: number; height: number } {
|
||||
const imetaTag = event.tags.find(tag => tag[0] === 'imeta');
|
||||
@@ -21,4 +22,26 @@ export function extractDimensions(event: NostrEvent): { width: number; height: n
|
||||
}
|
||||
}
|
||||
return { width: 500, height: 300 }; // Default dimensions if not found
|
||||
}
|
||||
|
||||
export async function signEvent(loginType: string | null, event: NostrEvent): Promise<NostrEvent | null> {
|
||||
// Sign event
|
||||
let eventSigned: NostrEvent = { ...event, sig: '' };
|
||||
if (loginType === 'extension') {
|
||||
eventSigned = await window.nostr.signEvent(event);
|
||||
} else if (loginType === 'amber') {
|
||||
// TODO: Sign event with amber
|
||||
alert('Signing with Amber is not implemented yet, sorry!');
|
||||
return null;
|
||||
} else if (loginType === 'raw_nsec') {
|
||||
if (typeof window !== 'undefined') {
|
||||
let nsecStr = null;
|
||||
nsecStr = window.localStorage.getItem('nsec');
|
||||
if (nsecStr != null) {
|
||||
eventSigned = finalizeEvent(event, hexToBytes(nsecStr));
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(eventSigned);
|
||||
return eventSigned;
|
||||
}
|
||||
Reference in New Issue
Block a user