feat: files uploader
This commit is contained in:
@@ -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
|
||||||
|
|||||||
80
src/renderer/src/components/PostDialog/Uploader.tsx
Normal file
80
src/renderer/src/components/PostDialog/Uploader.tsx
Normal 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/*"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user