"use client"
import { useState, useEffect, useCallback, type ChangeEvent, type FormEvent } from "react"
import { ReloadIcon } from "@radix-ui/react-icons"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import type { NostrEvent } from "nostr-tools"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { signEvent } from "@/lib/utils"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Copy, Check, ExternalLink, FileImage, Clock, Database, ArrowLeft } from "lucide-react"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import { Spinner } from "./spinner"
interface UploadResponse {
url: string
size: number
type: string
sha256: string
uploaded: number
nip94: {
tags: string[][]
content: string
}
}
function formatBytes(bytes: number, decimals = 2) {
if (bytes === 0) return "0 Bytes"
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ["Bytes", "KB", "MB", "GB", "TB"]
const i = Math.floor(Math.log(bytes) / Math.log(k))
return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]
}
function formatDate(timestamp: number) {
return new Date(timestamp * 1000).toLocaleString()
}
const CopyButton = ({ text }: { text: string }) => {
const [copied, setCopied] = useState(false)
const handleCopy = () => {
navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
{copied ? "Copied!" : "Copy to clipboard"}
)
}
const UploadResponseView = ({ data, onReset }: { data: UploadResponse; onReset: () => void }) => {
const dimensions = data.nip94.tags.find((tag) => tag[0] === "dim")?.[1] || ""
const blurhash = data.nip94.tags.find((tag) => tag[0] === "blurhash")?.[1] || ""
const thumbUrl = data.nip94.tags.find((tag) => tag[0] === "thumb")?.[1] || ""
return (
Upload Successful
{data.type}
File uploaded on {formatDate(data.uploaded)}
File Size
{formatBytes(data.size)}
Upload Time
{formatDate(data.uploaded)}
{dimensions && (
)}
URL
Hash
Metadata
{thumbUrl && (
)}
{blurhash && (
)}
{data.nip94.tags.map((tag, index) => (
{tag[0]}
{tag[1]}
))}
)
}
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.crossOrigin = "anonymous"
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) {
// Mock blurhash calculation - in a real app you'd use the blurhash library
const mockBlurhash = "LGF5?xYk^6#M@-5c,1J5@[or[Q6."
resolve(mockBlurhash)
} else {
reject(new Error("Failed to get image data"))
}
}
img.onerror = reject
img.src = URL.createObjectURL(file)
})
}
async function calculateSHA256(file: File): Promise {
// In a browser environment, we'd use the Web Crypto API
// This is a simplified mock implementation
return new Promise((resolve) => {
setTimeout(() => {
// Generate a random SHA256-like hash for demo purposes
const mockHash = Array.from({ length: 64 }, () => "0123456789abcdef"[Math.floor(Math.random() * 16)]).join("")
resolve(mockHash)
}, 500)
})
}
const UploadComponent = () => {
const { createHash } = require("crypto")
const loginType = typeof window !== "undefined" ? window.localStorage.getItem("loginType") : null
const [previewUrl, setPreviewUrl] = useState("")
const [isLoading, setIsLoading] = useState(false)
const [uploadedNoteId, setUploadedNoteId] = useState("")
const [retryCount, setRetryCount] = useState(0)
const [shouldFetch, setShouldFetch] = useState(false)
const [serverChoice, setServerChoice] = useState("blossom.band")
const [events, setEvents] = useState([])
const [isNoteLoading, setIsNoteLoading] = useState(false)
const [uploadResponse, setUploadResponse] = useState(null)
const [showUploadForm, setShowUploadForm] = useState(true)
useEffect(() => {
if (uploadedNoteId) {
setShouldFetch(true)
}
}, [uploadedNoteId])
useEffect(() => {
let timeoutId: NodeJS.Timeout
if (shouldFetch && events.length === 0 && !isNoteLoading) {
setIsNoteLoading(true)
// Simulate fetching events
timeoutId = setTimeout(() => {
setIsNoteLoading(false)
// After a few retries, simulate finding the event
if (retryCount >= 2) {
setEvents([{ id: uploadedNoteId }])
} else {
setRetryCount((prevCount) => prevCount + 1)
}
}, 2000)
}
return () => {
if (timeoutId) {
clearTimeout(timeoutId)
}
}
}, [shouldFetch, events, isNoteLoading, retryCount, uploadedNoteId])
const handleRetry = useCallback(() => {
setRetryCount((prevCount) => prevCount + 1)
setShouldFetch(false)
setTimeout(() => setShouldFetch(true), 100)
}, [])
const handleFileChange = (event: ChangeEvent) => {
const file = event.target.files?.[0]
if (file) {
const url = URL.createObjectURL(file)
setPreviewUrl(url)
return () => URL.revokeObjectURL(url)
}
}
const handleServerChange = (value: string) => {
setServerChoice(value)
}
const resetUpload = () => {
setUploadResponse(null)
setShowUploadForm(true)
setPreviewUrl("")
setUploadedNoteId("")
setEvents([])
setRetryCount(0)
setShouldFetch(false)
}
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
}
// If file is present, 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 for blossom auth via nostr
const authEvent: NostrEvent = {
kind: 24242,
content: "File upload",
created_at: createdAt,
tags: [
["t", "media"],
["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)
// Sign auth event
const authEventSigned = (await signEvent(loginType, authEvent)) as NostrEvent
// authEventSigned as base64 encoded string
const authString = Buffer.from(JSON.stringify(authEventSigned)).toString("base64")
const blossomServer = "https://" + serverChoice
await fetch(blossomServer + "/media", {
method: "PUT",
body: file,
headers: { authorization: "Nostr " + authString },
}).then(async (res) => {
if (res.ok) {
const responseText = await res.text()
const responseJson = JSON.parse(responseText)
finalFileUrl = responseJson.url
sha256 = responseJson.sha256
// Set the upload response data
setUploadResponse(responseJson)
let blurhash = ""
if (file && file.type.startsWith("image/")) {
try {
blurhash = await calculateBlurhash(file)
} catch (error) {
console.error("Error calculating blurhash:", error)
}
}
if (finalFileUrl) {
const image = new Image()
image.src = URL.createObjectURL(file)
await new Promise((resolve) => {
image.onload = resolve
})
finalNoteContent = desc
}
setIsLoading(false)
if (finalFileUrl != null) {
setShowUploadForm(false)
setShouldFetch(true)
setRetryCount(0)
}
} else {
throw new Error("Failed to upload file: " + (await res.text()))
}
})
} catch (error) {
alert(error)
console.error("Error reading file:", error)
setIsLoading(false)
}
}
}
return (
{showUploadForm ? (
Upload Image
Select an image to upload to {serverChoice}
) : uploadResponse ? (
) : (
Processing upload...
)}
)
}
export default UploadComponent