diff --git a/client-web/src/modules/chats/presentation/components/MessageInput.tsx b/client-web/src/modules/chats/presentation/components/MessageInput.tsx index fd7786c..39e0be3 100644 --- a/client-web/src/modules/chats/presentation/components/MessageInput.tsx +++ b/client-web/src/modules/chats/presentation/components/MessageInput.tsx @@ -331,65 +331,70 @@ export default function MessageInput({ chatId }: MessageInputProps) { }); }; - const handleFileChange = (e: React.ChangeEvent) => { - const files = Array.from(e.target.files || []); - if (files.length > 0) { - const { addNotification } = useNotificationStore.getState(); - const newAttachments: Attachment[] = []; - - let tooLarge = false; - let limitExceeded = false; + const processFiles = useCallback((files: File[]) => { + const { addNotification } = useNotificationStore.getState(); + const newAttachments: Attachment[] = []; + + let tooLarge = false; + let limitExceeded = false; - for (const file of files) { - if (attachments.length + newAttachments.length >= 20) { - limitExceeded = true; - break; - } - if (file.size > MAX_FILE_SIZE) { - tooLarge = true; - continue; - } - const isAudio = file.type.startsWith('audio/') || AUDIO_EXTENSIONS.some(ext => file.name.toLowerCase().endsWith(ext)); - newAttachments.push({ file, type: isAudio ? 'audio' : 'file' }); + for (const file of files) { + if (attachments.length + newAttachments.length >= 20) { + limitExceeded = true; + break; + } + if (file.size > MAX_FILE_SIZE) { + tooLarge = true; + continue; } - if (tooLarge) addNotification('warning', t('fileTooLarge') || 'Some files are too large'); - if (limitExceeded) addNotification('warning', t('maxFilesLimit' as any) || 'Maximum 20 files'); - - setAttachments(prev => [...prev, ...newAttachments]); - inputRef.current?.focus(); + const isVideo = file.type.startsWith('video/'); + const isImage = file.type.startsWith('image/'); + const isAudio = file.type.startsWith('audio/') || AUDIO_EXTENSIONS.some(ext => file.name.toLowerCase().endsWith(ext)); + + const type = isImage ? 'image' : isVideo ? 'video' : isAudio ? 'audio' : 'file'; + const preview = isImage ? URL.createObjectURL(file) : undefined; + + newAttachments.push({ file, type, preview }); } + + if (tooLarge) addNotification('warning', t('fileTooLarge') || 'Some files are too large'); + if (limitExceeded) addNotification('warning', t('maxFilesLimit' as any) || 'Maximum 20 files'); + + setAttachments(prev => [...prev, ...newAttachments]); + inputRef.current?.focus(); + }, [attachments, t]); + + const handleFileChange = (e: React.ChangeEvent) => { + const files = Array.from(e.target.files || []); + if (files.length > 0) processFiles(files); e.target.value = ''; setShowAttachMenu(false); }; const handleImageChange = (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); - if (files.length > 0) { - const { addNotification } = useNotificationStore.getState(); - const newAttachments: Attachment[] = []; - - let limitExceeded = false; - - for (const file of files) { - if (attachments.length + newAttachments.length >= 20) { - limitExceeded = true; - break; - } - const isVideo = file.type.startsWith('video/'); - const preview = file.type.startsWith('image/') ? URL.createObjectURL(file) : undefined; - newAttachments.push({ file, preview, type: isVideo ? 'video' : 'image' }); - } - - if (limitExceeded) addNotification('warning', t('maxFilesLimit' as any) || 'Maximum 20 files'); - - setAttachments(prev => [...prev, ...newAttachments]); - inputRef.current?.focus(); - } + if (files.length > 0) processFiles(files); e.target.value = ''; setShowAttachMenu(false); }; + const handlePaste = (e: React.ClipboardEvent) => { + const items = Array.from(e.clipboardData.items); + const files = items + .filter(item => item.kind === 'file') + .map(item => item.getAsFile()) + .filter((f): f is File => f !== null); + + if (files.length > 0) { + processFiles(files); + // If we only pasted files, don't paste the filename/text representation in the textarea + if (items.every(item => item.kind === 'file')) { + e.preventDefault(); + } + } + }; + // Запись голосового const startRecording = async () => { try { @@ -569,41 +574,11 @@ export default function MessageInput({ chatId }: MessageInputProps) { if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { const files = Array.from(e.dataTransfer.files); - const { addNotification } = useNotificationStore.getState(); - const newAttachments: Attachment[] = []; - - let tooLarge = false; - let limitExceeded = false; - - for (const file of files) { - if (attachments.length + newAttachments.length >= 20) { - limitExceeded = true; - break; - } - if (file.size > MAX_FILE_SIZE) { - tooLarge = true; - continue; - } - - const isVideo = file.type.startsWith('video/'); - const isImage = file.type.startsWith('image/'); - const audioExts = ['.mp3', '.wav', '.ogg', '.m4a', '.aac', '.flac', '.wma', '.opus']; - const isAudio = file.type.startsWith('audio/') || audioExts.some(ext => file.name.toLowerCase().endsWith(ext)); - - const type = isImage ? 'image' : isVideo ? 'video' : isAudio ? 'audio' : 'file'; - const preview = isImage ? URL.createObjectURL(file) : undefined; - - newAttachments.push({ file, type, preview }); - } - - if (tooLarge) addNotification('warning', t('fileTooLarge') || 'Some files are too large'); - if (limitExceeded) addNotification('warning', t('maxFilesLimit' as any) || 'Maximum 20 files'); - - setAttachments(prev => [...prev, ...newAttachments]); - inputRef.current?.focus(); + processFiles(files); } }; + const hasContent = text.trim() || attachments.length > 0; return ( @@ -856,6 +831,7 @@ export default function MessageInput({ chatId }: MessageInputProps) { }} onKeyDown={handleKeyDown} onContextMenu={handleInputContextMenu} + onPaste={handlePaste} rows={1} className="w-full bg-transparent border-none focus:ring-0 text-[#efeff3] placeholder-on-surface-variant/30 text-[16px] leading-[1.3] resize-none max-h-[140px] custom-scrollbar outline-none py-1 px-0" placeholder={attachments.length > 0 ? t('addCaption') : t('messagePlaceholder') || 'Сообщение...'}