feat: files uploader

This commit is contained in:
codytseng
2024-11-10 01:13:11 +08:00
parent 87dbc33231
commit af04aed6e8
4 changed files with 122 additions and 20 deletions

View File

@@ -104,7 +104,7 @@ function preprocess(content: string) {
function isImage(url: string) { function isImage(url: string) {
try { try {
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', 'webp', 'heic', 'svg'] const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.heic', '.svg']
return imageExtensions.some((ext) => new URL(url).pathname.toLowerCase().endsWith(ext)) return imageExtensions.some((ext) => new URL(url).pathname.toLowerCase().endsWith(ext))
} catch { } catch {
return false return false
@@ -113,7 +113,7 @@ function isImage(url: string) {
function isVideo(url: string) { function isVideo(url: string) {
try { try {
const videoExtensions = ['.mp4', '.webm', '.ogg'] const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov']
return videoExtensions.some((ext) => new URL(url).pathname.toLowerCase().endsWith(ext)) return videoExtensions.some((ext) => new URL(url).pathname.toLowerCase().endsWith(ext))
} catch { } catch {
return false return false

View File

@@ -0,0 +1,80 @@
import { Button } from '@renderer/components/ui/button'
import { useToast } from '@renderer/hooks/use-toast'
import { useNostr } from '@renderer/providers/NostrProvider'
import { ImageUp, LoaderCircle } from 'lucide-react'
import { useRef, useState } from 'react'
export default function Uploader({
setContent
}: {
setContent: React.Dispatch<React.SetStateAction<string>>
}) {
const [uploading, setUploading] = useState(false)
const { signHttpAuth } = useNostr()
const { toast } = useToast()
const fileInputRef = useRef<HTMLInputElement>(null)
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (!file) return
const formData = new FormData()
formData.append('file', file)
try {
setUploading(true)
const url = 'https://nostr.build/api/v2/nip96/upload'
const auth = await signHttpAuth(url, 'POST')
const response = await fetch(url, {
method: 'POST',
body: formData,
headers: {
Authorization: auth
}
})
if (!response.ok) {
throw new Error(response.status.toString())
}
const data = await response.json()
const imageUrl = data.nip94_event?.tags.find(([tagName]) => tagName === 'url')?.[1]
if (imageUrl) {
setContent((prevContent) => `${prevContent}\n${imageUrl}`)
} else {
throw new Error('No image url found')
}
} catch (error) {
console.error('Error uploading file', error)
toast({
variant: 'destructive',
title: 'Failed to upload file',
description: (error as Error).message
})
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
} finally {
setUploading(false)
}
}
const handleUploadClick = () => {
fileInputRef.current?.click()
}
return (
<>
<Button variant="secondary" onClick={handleUploadClick} disabled={uploading}>
{uploading ? <LoaderCircle className="animate-spin" /> : <ImageUp />}
</Button>
<input
type="file"
ref={fileInputRef}
style={{ display: 'none' }}
onChange={handleFileChange}
accept="image/*,video/*,audio/*"
/>
</>
)
}

View File

@@ -2,7 +2,6 @@ import { Button } from '@renderer/components/ui/button'
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger DialogTrigger
@@ -19,6 +18,7 @@ import { useState } from 'react'
import UserAvatar from '../UserAvatar' import UserAvatar from '../UserAvatar'
import Mentions from './Metions' import Mentions from './Metions'
import Preview from './Preview' import Preview from './Preview'
import Uploader from './Uploader'
export default function PostDialog({ export default function PostDialog({
children, children,
@@ -107,7 +107,9 @@ export default function PostDialog({
placeholder="Write something..." placeholder="Write something..."
/> />
{content && <Preview content={content} />} {content && <Preview content={content} />}
<DialogFooter className="items-center"> <div className="flex items-center justify-between">
<Uploader setContent={setContent} />
<div className="flex gap-2">
<Mentions content={content} parentEvent={parentEvent} /> <Mentions content={content} parentEvent={parentEvent} />
<Button <Button
variant="secondary" variant="secondary"
@@ -122,7 +124,8 @@ export default function PostDialog({
{posting && <LoaderCircle className="animate-spin" />} {posting && <LoaderCircle className="animate-spin" />}
Post Post
</Button> </Button>
</DialogFooter> </div>
</div>
</div> </div>
</ScrollArea> </ScrollArea>
</DialogContent> </DialogContent>

View File

@@ -1,6 +1,8 @@
import { TDraftEvent } from '@common/types' import { TDraftEvent } from '@common/types'
import { useFetchRelayList } from '@renderer/hooks/useFetchRelayList' import { useFetchRelayList } from '@renderer/hooks/useFetchRelayList'
import client from '@renderer/services/client.service' import client from '@renderer/services/client.service'
import dayjs from 'dayjs'
import { kinds } from 'nostr-tools'
import { createContext, useContext, useEffect, useState } from 'react' import { createContext, useContext, useEffect, useState } from 'react'
type TNostrContext = { type TNostrContext = {
@@ -12,6 +14,7 @@ type TNostrContext = {
* Default publish the event to current relays, user's write relays and additional relays * Default publish the event to current relays, user's write relays and additional relays
*/ */
publish: (draftEvent: TDraftEvent, additionalRelayUrls?: string[]) => Promise<void> publish: (draftEvent: TDraftEvent, additionalRelayUrls?: string[]) => Promise<void>
signHttpAuth: (url: string, method: string) => Promise<string>
} }
const NostrContext = createContext<TNostrContext | undefined>(undefined) const NostrContext = createContext<TNostrContext | undefined>(undefined)
@@ -65,8 +68,24 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
await client.publishEvent(relayList.write.concat(additionalRelayUrls), event) await client.publishEvent(relayList.write.concat(additionalRelayUrls), event)
} }
const signHttpAuth = async (url: string, method: string) => {
const event = await window.api.nostr.signEvent({
content: '',
kind: kinds.HTTPAuth,
created_at: dayjs().unix(),
tags: [
['u', url],
['method', method]
]
})
if (!event) {
throw new Error('sign event failed')
}
return 'Nostr ' + btoa(JSON.stringify(event))
}
return ( return (
<NostrContext.Provider value={{ pubkey, canLogin, login, logout, publish }}> <NostrContext.Provider value={{ pubkey, canLogin, login, logout, publish, signHttpAuth }}>
{children} {children}
</NostrContext.Provider> </NostrContext.Provider>
) )