diff --git a/src/renderer/src/components/Content/index.tsx b/src/renderer/src/components/Content/index.tsx index be400fb..1a32d33 100644 --- a/src/renderer/src/components/Content/index.tsx +++ b/src/renderer/src/components/Content/index.tsx @@ -104,7 +104,7 @@ function preprocess(content: string) { function isImage(url: string) { 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)) } catch { return false @@ -113,7 +113,7 @@ function isImage(url: string) { function isVideo(url: string) { try { - const videoExtensions = ['.mp4', '.webm', '.ogg'] + const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov'] return videoExtensions.some((ext) => new URL(url).pathname.toLowerCase().endsWith(ext)) } catch { return false diff --git a/src/renderer/src/components/PostDialog/Uploader.tsx b/src/renderer/src/components/PostDialog/Uploader.tsx new file mode 100644 index 0000000..4ec633b --- /dev/null +++ b/src/renderer/src/components/PostDialog/Uploader.tsx @@ -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> +}) { + const [uploading, setUploading] = useState(false) + const { signHttpAuth } = useNostr() + const { toast } = useToast() + const fileInputRef = useRef(null) + + const handleFileChange = async (event: React.ChangeEvent) => { + 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 ( + <> + + + + ) +} diff --git a/src/renderer/src/components/PostDialog/index.tsx b/src/renderer/src/components/PostDialog/index.tsx index 32c9087..a15945d 100644 --- a/src/renderer/src/components/PostDialog/index.tsx +++ b/src/renderer/src/components/PostDialog/index.tsx @@ -2,7 +2,6 @@ import { Button } from '@renderer/components/ui/button' import { Dialog, DialogContent, - DialogFooter, DialogHeader, DialogTitle, DialogTrigger @@ -19,6 +18,7 @@ import { useState } from 'react' import UserAvatar from '../UserAvatar' import Mentions from './Metions' import Preview from './Preview' +import Uploader from './Uploader' export default function PostDialog({ children, @@ -107,22 +107,25 @@ export default function PostDialog({ placeholder="Write something..." /> {content && } - - - - - +
+ +
+ + + +
+
diff --git a/src/renderer/src/providers/NostrProvider.tsx b/src/renderer/src/providers/NostrProvider.tsx index d433a60..1786a48 100644 --- a/src/renderer/src/providers/NostrProvider.tsx +++ b/src/renderer/src/providers/NostrProvider.tsx @@ -1,6 +1,8 @@ import { TDraftEvent } from '@common/types' import { useFetchRelayList } from '@renderer/hooks/useFetchRelayList' import client from '@renderer/services/client.service' +import dayjs from 'dayjs' +import { kinds } from 'nostr-tools' import { createContext, useContext, useEffect, useState } from 'react' type TNostrContext = { @@ -12,6 +14,7 @@ type TNostrContext = { * Default publish the event to current relays, user's write relays and additional relays */ publish: (draftEvent: TDraftEvent, additionalRelayUrls?: string[]) => Promise + signHttpAuth: (url: string, method: string) => Promise } const NostrContext = createContext(undefined) @@ -65,8 +68,24 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { 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 ( - + {children} )