check uploaded note by fetching it. showing this in a drawer for now

This commit is contained in:
mr0x50
2025-01-24 17:42:16 +01:00
parent 883a66590e
commit ce82b3175a
2 changed files with 130 additions and 68 deletions

View File

@@ -1,7 +1,7 @@
import { useNostr } from "nostr-react"
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 } 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"
@@ -10,6 +10,15 @@ import { Label } from "./ui/label"
import { Input } from "./ui/input"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
import { encode } from "blurhash"
import {
Drawer,
DrawerContent,
DrawerHeader,
DrawerTitle,
DrawerDescription,
DrawerFooter,
} from "@/components/ui/drawer"
import { Spinner } from "@/components/spinner"
async function signEvent(loginType: string | null, authEvent: string) {
let authEventSigned = {}
@@ -27,7 +36,7 @@ async function signEvent(loginType: string | null, authEvent: string) {
}
}
}
return authEventSigned;
return authEventSigned
}
async function calculateBlurhash(file: File): Promise<string> {
@@ -59,6 +68,51 @@ const UploadComponent: React.FC = () => {
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 { 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]
@@ -97,7 +151,7 @@ const UploadComponent: React.FC = () => {
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) => {
@@ -134,24 +188,7 @@ const UploadComponent: React.FC = () => {
console.log(authEvent)
// Sign auth event
let authEventSigned = await signEvent(loginType, JSON.stringify(authEvent))
// ----
// 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)
const authEventSigned = await signEvent(loginType, JSON.stringify(authEvent))
// Actually upload the file
await fetch("https://void.cat/upload", {
@@ -160,9 +197,7 @@ const UploadComponent: React.FC = () => {
headers: { authorization: "Nostr " + btoa(JSON.stringify(authEventSigned)) },
}).then(async (res) => {
if (res.ok) {
// alert('File uploaded successfully');
const responseText = await res.text()
// alert(responseText);
const responseJson = JSON.parse(responseText)
finalFileUrl = responseJson.url
} else {
@@ -186,21 +221,6 @@ const UploadComponent: React.FC = () => {
}
}
// 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) {
const image = new Image()
image.src = URL.createObjectURL(file)
@@ -233,33 +253,21 @@ const UploadComponent: React.FC = () => {
let signedEvent: NostrEvent | null = null
// Sign the actual note
signedEvent = await signEvent(loginType, JSON.stringify(noteEvent)) as NostrEvent
// 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))
// }
// }
// }
signedEvent = (await signEvent(loginType, JSON.stringify(noteEvent))) as NostrEvent
// If the got a signed event, publish it to nostr
// If we 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)
setUploadedNoteId(signedEvent.id)
setIsDrawerOpen(true)
setShouldFetch(true)
setRetryCount(0)
}
}
@@ -274,17 +282,10 @@ const UploadComponent: React.FC = () => {
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 || "/placeholder.svg"} alt="Preview" className="w-full pt-4" />}
</AccordionContent>
</AccordionItem>
</Accordion>
<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" />
@@ -294,6 +295,47 @@ const UploadComponent: React.FC = () => {
)}
</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>
</>
)
}

View 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>
)
}