diff --git a/src/components/PostEditor/NormalPostContent.tsx b/src/components/PostEditor/NormalPostContent.tsx index 7a437800..7b4808f8 100644 --- a/src/components/PostEditor/NormalPostContent.tsx +++ b/src/components/PostEditor/NormalPostContent.tsx @@ -1,6 +1,7 @@ import Note from '@/components/Note' import { Button } from '@/components/ui/button' import { ScrollArea } from '@/components/ui/scroll-area' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { useToast } from '@/hooks/use-toast' import { createCommentDraftEvent, createShortTextNoteDraftEvent } from '@/lib/draft-event' import { useNostr } from '@/providers/NostrProvider' @@ -9,7 +10,7 @@ import { ChevronDown, ImageUp, LoaderCircle } from 'lucide-react' import { Event, kinds } from 'nostr-tools' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import TextareaWithMentions from '../TextareaWithMentions' +import PostTextarea from '../PostTextarea' import Mentions from './Mentions' import PostOptions from './PostOptions' import Preview from './Preview' @@ -123,14 +124,29 @@ export default function NormalPostContent({ )} - - {processedContent && } + + + {t('Edit')} + {t('Preview')} + + + { + setPictureInfos((prev) => [...prev, { url, tags }]) + }} + /> + + + + + { setPictureInfos((prev) => [...prev, { url, tags }]) - setContent((prev) => `${prev}\n${url}`) + setContent((prev) => (prev === '' ? url : `${prev}\n${url}`)) }} onUploadingChange={setUploadingPicture} accept="image/*,video/*,audio/*" diff --git a/src/components/PostEditor/PicturePostContent.tsx b/src/components/PostEditor/PicturePostContent.tsx index 936367d9..7209ab89 100644 --- a/src/components/PostEditor/PicturePostContent.tsx +++ b/src/components/PostEditor/PicturePostContent.tsx @@ -8,7 +8,7 @@ import { ChevronDown, Loader, LoaderCircle, Plus, X } from 'lucide-react' import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import Image from '../Image' -import TextareaWithMentions from '../TextareaWithMentions' +import PostTextarea from '../PostTextarea' import Mentions from './Mentions' import PostOptions from './PostOptions' import SendOnlyToSwitch from './SendOnlyToSwitch' @@ -105,7 +105,7 @@ export default function PicturePostContent({ close }: { close: () => void }) { {t('A special note for picture-first clients like Olas')} - + ) diff --git a/src/components/PostEditor/Uploader.tsx b/src/components/PostEditor/Uploader.tsx index 30e2ae21..5fed685e 100644 --- a/src/components/PostEditor/Uploader.tsx +++ b/src/components/PostEditor/Uploader.tsx @@ -20,14 +20,15 @@ export default function Uploader({ const fileInputRef = useRef(null) const handleFileChange = async (event: React.ChangeEvent) => { - const file = event.target.files?.[0] - if (!file) return + if (!event.target.files) return + onUploadingChange?.(true) try { - onUploadingChange?.(true) - const result = await upload(file) - console.log('File uploaded successfully', result) - onUploadSuccess(result) + for (const file of event.target.files) { + const result = await upload(file) + console.log('File uploaded successfully', result) + onUploadSuccess(result) + } } catch (error) { console.error('Error uploading file', error) toast({ @@ -59,6 +60,7 @@ export default function Uploader({ style={{ display: 'none' }} onChange={handleFileChange} accept={accept} + multiple /> ) diff --git a/src/components/PostEditor/index.tsx b/src/components/PostEditor/index.tsx index 15fe462c..937ea402 100644 --- a/src/components/PostEditor/index.tsx +++ b/src/components/PostEditor/index.tsx @@ -6,14 +6,17 @@ import { DialogTitle } from '@/components/ui/dialog' import { ScrollArea } from '@/components/ui/scroll-area' -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle +} from '@/components/ui/sheet' import { useScreenSize } from '@/providers/ScreenSizeProvider' import { Event } from 'nostr-tools' import { Dispatch, useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '../ui/sheet' import NormalPostContent from './NormalPostContent' -import PicturePostContent from './PicturePostContent' import Title from './Title' export default function PostEditor({ @@ -27,35 +30,17 @@ export default function PostEditor({ open: boolean setOpen: Dispatch }) { - const { t } = useTranslation() const { isSmallScreen } = useScreenSize() const content = useMemo(() => { - return parentEvent || defaultContent ? ( + return ( setOpen(false)} /> - ) : ( - - - {t('Normal Note')} - {t('Picture Note')} - - - setOpen(false)} - /> - - - setOpen(false)} /> - - ) - }, [parentEvent, defaultContent]) + }, []) if (isSmallScreen) { return ( diff --git a/src/components/TextareaWithMentions/index.tsx b/src/components/PostTextarea/index.tsx similarity index 72% rename from src/components/TextareaWithMentions/index.tsx rename to src/components/PostTextarea/index.tsx index da17bcdc..c706052d 100644 --- a/src/components/TextareaWithMentions/index.tsx +++ b/src/components/PostTextarea/index.tsx @@ -1,7 +1,9 @@ import { Command, CommandInput, CommandItem, CommandList } from '@/components/ui/command' import { Textarea } from '@/components/ui/textarea' +import { useToast } from '@/hooks' import { pubkeyToNpub } from '@/lib/pubkey' import { cn } from '@/lib/utils' +import { useMediaUploadService } from '@/providers/MediaUploadServiceProvider' import client from '@/services/client.service' import { TProfile } from '@/types' import React, { @@ -18,22 +20,27 @@ import { SimpleUserAvatar } from '../UserAvatar' import { SimpleUsername } from '../Username' import { getCurrentWord, replaceWord } from './utils' -export default function TextareaWithMentions({ +export default function PostTextarea({ textValue, setTextValue, cursorOffset = 0, + onUploadImage, ...props }: ComponentProps<'textarea'> & { textValue: string setTextValue: Dispatch> cursorOffset?: number + onUploadImage?: ({ url, tags }: { url: string; tags: string[][] }) => void }) { + const { toast } = useToast() const textareaRef = useRef(null) const dropdownRef = useRef(null) const inputRef = useRef(null) + const { upload } = useMediaUploadService() const [commandValue, setCommandValue] = useState('') const [debouncedCommandValue, setDebouncedCommandValue] = useState(commandValue) const [profiles, setProfiles] = useState([]) + const [dragover, setDragover] = useState(false) useEffect(() => { if (textareaRef.current && cursorOffset !== 0) { @@ -132,7 +139,7 @@ export default function TextareaWithMentions({ const textarea = textareaRef.current const dropdown = dropdownRef.current if (textarea && dropdown) { - replaceWord(textarea, `${value}`) + replaceWord(textarea, `${value} `) setCommandValue('') dropdown.classList.add('hidden') } @@ -170,9 +177,73 @@ export default function TextareaWithMentions({ } }, [handleBlur, handleKeyDown, handleMouseDown, handleSectionChange]) + const uploadImages = async (files: File[]) => { + for (const file of files) { + if (file.type.startsWith('image/') || file.type.startsWith('video/')) { + const placeholder = `[Uploading "${file.name}"...]` + if (textValue.includes(placeholder)) { + continue + } + setTextValue((prev) => (prev === '' ? placeholder : `${prev}\n${placeholder}`)) + try { + const result = await upload(file) + setTextValue((prev) => { + if (prev.includes(placeholder)) { + return prev.replace(placeholder, result.url) + } else { + return prev + `\n${result.url}` + } + }) + onUploadImage?.(result) + } catch (error) { + console.error('Error uploading file', error) + toast({ + variant: 'destructive', + title: 'Failed to upload file', + description: (error as Error).message + }) + setTextValue((prev) => prev.replace(placeholder, '')) + } + } + } + } + + const handlePast = async (e: React.ClipboardEvent) => { + await uploadImages( + Array.from(e.clipboardData.items) + .map((item) => item.getAsFile()) + .filter(Boolean) as File[] + ) + } + + const handleDrop = async (e: React.DragEvent) => { + e.preventDefault() + setDragover(false) + await uploadImages(Array.from(e.dataTransfer.files)) + } + return ( - - + + { + e.preventDefault() + setDragover(true) + }} + onDragLeave={() => { + setDragover(false) + }} + />