Улучшены стили и функциональность компонентов мессенджера, включая поддержку многострочного ввода сообщений и обновление стилей кнопок. Добавлен новый метод скачивания файлов через API для изображений и документов с обработкой ошибок. Оптимизированы компоненты для загрузки файлов и записи голоса, улучшен интерфейс выбора эмодзи.

This commit is contained in:
Bivekich
2025-07-17 13:06:40 +03:00
parent abd0df1fe7
commit e191055682
9 changed files with 412 additions and 78 deletions

View File

@ -360,7 +360,7 @@ export function MessengerChat({ counterparty }: MessengerChatProps) {
)}
<div className={`flex ${isCurrentUser ? 'justify-end' : 'justify-start'} mb-3`}>
<div className="flex flex-col max-w-xs lg:max-w-md">
<div className="flex flex-col max-w-xs lg:max-w-md break-words">
{/* Имя отправителя */}
{!isCurrentUser && (
<div className="flex items-center space-x-2 mb-1 px-1">
@ -409,12 +409,12 @@ export function MessengerChat({ counterparty }: MessengerChatProps) {
isCurrentUser={isCurrentUser}
/>
) : msg.content ? (
<div className={`px-4 py-2 rounded-lg ${
<div className={`px-4 py-2 rounded-lg break-words ${
isCurrentUser
? 'bg-blue-500/20 text-white border border-blue-500/30'
: 'bg-white/10 text-white border border-white/20'
}`}>
<p className="text-sm leading-relaxed">{msg.content}</p>
<p className="text-sm leading-relaxed whitespace-pre-wrap break-words">{msg.content}</p>
</div>
) : null}
@ -434,28 +434,42 @@ export function MessengerChat({ counterparty }: MessengerChatProps) {
</div>
{/* Поле ввода сообщения */}
<div className="p-4 border-t border-white/10">
<div className="flex space-x-2">
<div className="flex flex-1 space-x-2">
<Input
<div className="px-4 py-3 border-t border-white/10">
<div className="flex items-center space-x-2">
<div className="flex-1">
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Введите сообщение..."
className="glass-input text-white placeholder:text-white/40 flex-1"
className="glass-input text-white placeholder:text-white/40 w-full resize-none overflow-y-auto rounded-lg py-2 px-3"
rows={1}
style={{
height: 'auto',
minHeight: '40px',
maxHeight: '120px'
}}
onInput={(e) => {
const target = e.target as HTMLTextAreaElement
target.style.height = 'auto'
const newHeight = Math.min(Math.max(target.scrollHeight, 40), 120)
target.style.height = newHeight + 'px'
}}
/>
</div>
<div className="flex items-center gap-2">
<EmojiPickerComponent onEmojiSelect={handleEmojiSelect} />
<FileUploader onSendFile={handleSendFile} />
<VoiceRecorder onSendVoice={handleSendVoice} />
<Button
onClick={handleSendMessage}
disabled={!message.trim()}
className="bg-blue-500/20 hover:bg-blue-500/30 text-blue-300 border-blue-500/30 cursor-pointer h-10 w-10 p-0"
variant="outline"
>
<Send className="h-4 w-4" />
</Button>
</div>
<Button
onClick={handleSendMessage}
disabled={!message.trim()}
className="bg-blue-500/20 hover:bg-blue-500/30 text-blue-300 border-blue-500/30 cursor-pointer"
variant="outline"
>
<Send className="h-4 w-4" />
</Button>
</div>
</div>
</div>

View File

@ -45,9 +45,8 @@ export function EmojiPickerComponent({ onEmojiSelect }: EmojiPickerComponentProp
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setShowPicker(!showPicker)}
className="text-white/60 hover:text-white hover:bg-white/10 p-2"
className="text-white/60 hover:text-white hover:bg-white/10 h-10 w-10 p-0"
>
<Smile className="h-4 w-4" />
</Button>

View File

@ -39,13 +39,43 @@ export function FileMessage({ fileUrl, fileName, fileSize, fileType, isCurrentUs
return parts.length > 1 ? parts.pop()?.toUpperCase() : 'FILE'
}
const handleDownload = () => {
const link = document.createElement('a')
link.href = fileUrl
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
const handleDownload = async () => {
console.log('🔽 Начинаем скачивание файла:', fileName, fileUrl)
try {
// Используем наш API endpoint для проксирования скачивания
const downloadUrl = `/api/download-file?url=${encodeURIComponent(fileUrl)}&filename=${encodeURIComponent(fileName)}`
console.log('📡 Скачиваем через API endpoint:', downloadUrl)
// Создаем ссылку для прямого скачивания
const link = document.createElement('a')
link.href = downloadUrl
link.download = fileName || 'file'
link.style.display = 'none'
document.body.appendChild(link)
console.log('⬇️ Запускаем скачивание через API...')
link.click()
// Очистка с задержкой
setTimeout(() => {
try {
document.body.removeChild(link)
console.log('✅ Скачивание через API успешно запущено')
} catch (cleanupError) {
console.warn('⚠️ Ошибка очистки:', cleanupError)
}
}, 1000)
} catch (error) {
console.error('❌ Ошибка скачивания через API:', error)
fallbackDownload()
}
function fallbackDownload() {
console.log('🚨 Используем fallback метод - открытие в новой вкладке')
window.open(fileUrl, '_blank')
}
}
return (

View File

@ -167,15 +167,14 @@ export function FileUploader({ onSendFile }: FileUploaderProps) {
}
return (
<div className="flex items-center space-x-2">
<>
{/* Кнопка для загрузки изображений */}
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => imageInputRef.current?.click()}
disabled={isUploading}
className="text-white/60 hover:text-white hover:bg-white/10 p-2"
className="text-white/60 hover:text-white hover:bg-white/10 h-10 w-10 p-0"
>
<Image className="h-4 w-4" />
</Button>
@ -184,10 +183,9 @@ export function FileUploader({ onSendFile }: FileUploaderProps) {
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => fileInputRef.current?.click()}
disabled={isUploading}
className="text-white/60 hover:text-white hover:bg-white/10 p-2"
className="text-white/60 hover:text-white hover:bg-white/10 h-10 w-10 p-0"
>
<Paperclip className="h-4 w-4" />
</Button>
@ -214,6 +212,6 @@ export function FileUploader({ onSendFile }: FileUploaderProps) {
Загрузка...
</div>
)}
</div>
</>
)
}

View File

@ -0,0 +1,205 @@
"use client"
import { useState, useEffect } from 'react'
import { X, Download, ZoomIn, ZoomOut } from 'lucide-react'
import { Button } from '@/components/ui/button'
interface ImageLightboxProps {
imageUrl: string
fileName: string
fileSize?: number
isOpen: boolean
onClose: () => void
}
export function ImageLightbox({ imageUrl, fileName, fileSize, isOpen, onClose }: ImageLightboxProps) {
const [zoom, setZoom] = useState(1)
const [position, setPosition] = useState({ x: 0, y: 0 })
const [isDragging, setIsDragging] = useState(false)
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
// Сброс состояния при открытии/закрытии
useEffect(() => {
if (isOpen) {
setZoom(1)
setPosition({ x: 0, y: 0 })
}
}, [isOpen])
// Закрытие по ESC
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen) {
onClose()
}
}
if (isOpen) {
document.addEventListener('keydown', handleKeyDown)
document.body.style.overflow = 'hidden'
}
return () => {
document.removeEventListener('keydown', handleKeyDown)
document.body.style.overflow = 'unset'
}
}, [isOpen, onClose])
const formatFileSize = (bytes?: number) => {
if (!bytes) return ''
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}
const handleDownload = async () => {
console.log('🔽 Скачиваем изображение из lightbox:', fileName)
try {
const downloadUrl = `/api/download-file?url=${encodeURIComponent(imageUrl)}&filename=${encodeURIComponent(fileName)}`
const link = document.createElement('a')
link.href = downloadUrl
link.download = fileName || 'image'
link.style.display = 'none'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
console.log('✅ Скачивание запущено из lightbox')
} catch (error) {
console.error('❌ Ошибка скачивания из lightbox:', error)
window.open(imageUrl, '_blank')
}
}
const handleZoomIn = () => {
setZoom(prev => Math.min(prev * 1.5, 5))
}
const handleZoomOut = () => {
setZoom(prev => Math.max(prev / 1.5, 0.5))
}
const handleMouseDown = (e: React.MouseEvent) => {
if (zoom > 1) {
setIsDragging(true)
setDragStart({
x: e.clientX - position.x,
y: e.clientY - position.y
})
}
}
const handleMouseMove = (e: React.MouseEvent) => {
if (isDragging && zoom > 1) {
setPosition({
x: e.clientX - dragStart.x,
y: e.clientY - dragStart.y
})
}
}
const handleMouseUp = () => {
setIsDragging(false)
}
if (!isOpen) return null
return (
<div className="fixed inset-0 z-50 bg-black/90 flex items-center justify-center">
{/* Фон для закрытия */}
<div
className="absolute inset-0"
onClick={onClose}
/>
{/* Панель управления */}
<div className="absolute top-4 right-4 flex items-center space-x-2 z-10">
<Button
onClick={handleZoomOut}
variant="outline"
size="icon"
className="bg-black/50 border-white/20 text-white hover:bg-white/10"
>
<ZoomOut className="h-4 w-4" />
</Button>
<Button
onClick={handleZoomIn}
variant="outline"
size="icon"
className="bg-black/50 border-white/20 text-white hover:bg-white/10"
>
<ZoomIn className="h-4 w-4" />
</Button>
<Button
onClick={handleDownload}
variant="outline"
size="icon"
className="bg-black/50 border-white/20 text-white hover:bg-white/10"
>
<Download className="h-4 w-4" />
</Button>
<Button
onClick={onClose}
variant="outline"
size="icon"
className="bg-black/50 border-white/20 text-white hover:bg-white/10"
>
<X className="h-4 w-4" />
</Button>
</div>
{/* Информация о файле */}
<div className="absolute top-4 left-4 z-10">
<div className="bg-black/50 backdrop-blur rounded-lg px-3 py-2">
<p className="text-white font-medium text-sm">{fileName}</p>
{fileSize && (
<p className="text-white/60 text-xs">{formatFileSize(fileSize)}</p>
)}
<p className="text-white/60 text-xs">Zoom: {Math.round(zoom * 100)}%</p>
</div>
</div>
{/* Изображение */}
<div className="relative max-w-full max-h-full overflow-hidden">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={imageUrl}
alt={fileName}
className={`max-w-full max-h-full object-contain transition-transform ${
zoom > 1 ? 'cursor-move' : 'cursor-zoom-in'
}`}
style={{
transform: `scale(${zoom}) translate(${position.x / zoom}px, ${position.y / zoom}px)`,
}}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
onClick={(e) => {
e.stopPropagation()
if (zoom === 1) {
handleZoomIn()
}
}}
draggable={false}
/>
</div>
{/* Инструкция */}
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 z-10">
<div className="bg-black/50 backdrop-blur rounded-lg px-4 py-2">
<p className="text-white/80 text-xs text-center">
ESC - закрыть Клик - увеличить Перетаскивание при зуме
</p>
</div>
</div>
</div>
)
}

View File

@ -2,8 +2,9 @@
import { useState } from 'react'
import Image from 'next/image'
import { Download, Eye } from 'lucide-react'
import { Download, Maximize2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { ImageLightbox } from '@/components/ui/image-lightbox'
interface ImageMessageProps {
imageUrl: string
@ -14,7 +15,7 @@ interface ImageMessageProps {
export function ImageMessage({ imageUrl, fileName, fileSize, isCurrentUser = false }: ImageMessageProps) {
const [isLoading, setIsLoading] = useState(true)
const [showFullSize, setShowFullSize] = useState(false)
const [isLightboxOpen, setIsLightboxOpen] = useState(false)
const formatFileSize = (bytes?: number) => {
if (!bytes) return ''
@ -24,17 +25,52 @@ export function ImageMessage({ imageUrl, fileName, fileSize, isCurrentUser = fal
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}
const handleDownload = () => {
const link = document.createElement('a')
link.href = imageUrl
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
const handleDownload = async (e: React.MouseEvent) => {
e.stopPropagation()
console.log('🔽 Начинаем скачивание изображения:', fileName, imageUrl)
try {
// Используем наш API endpoint для проксирования скачивания
const downloadUrl = `/api/download-file?url=${encodeURIComponent(imageUrl)}&filename=${encodeURIComponent(fileName)}`
console.log('📡 Скачиваем через API endpoint:', downloadUrl)
// Создаем ссылку для прямого скачивания
const link = document.createElement('a')
link.href = downloadUrl
link.download = fileName || 'image'
link.style.display = 'none'
document.body.appendChild(link)
console.log('⬇️ Запускаем скачивание через API...')
link.click()
// Очистка с задержкой
setTimeout(() => {
try {
document.body.removeChild(link)
console.log('✅ Скачивание изображения через API успешно запущено')
} catch (cleanupError) {
console.warn('⚠️ Ошибка очистки:', cleanupError)
}
}, 1000)
} catch (error) {
console.error('❌ Ошибка скачивания через API:', error)
fallbackDownload()
}
function fallbackDownload() {
console.log('🚨 Используем fallback метод - открытие в новой вкладке')
window.open(imageUrl, '_blank')
}
}
const handleImageClick = () => {
setShowFullSize(true)
const handleOpenLightbox = () => {
setIsLightboxOpen(true)
}
const handleCloseLightbox = () => {
setIsLightboxOpen(false)
}
return (
@ -57,7 +93,7 @@ export function ImageMessage({ imageUrl, fileName, fileSize, isCurrentUser = fal
objectFit: 'cover'
}}
onLoad={() => setIsLoading(false)}
onClick={handleImageClick}
onClick={handleOpenLightbox}
/>
{isLoading && (
@ -69,18 +105,20 @@ export function ImageMessage({ imageUrl, fileName, fileSize, isCurrentUser = fal
{/* Overlay с кнопками при наведении */}
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center justify-center space-x-2">
<Button
onClick={handleImageClick}
onClick={handleOpenLightbox}
size="sm"
variant="ghost"
className="text-white hover:bg-white/20"
title="Открыть в полном размере"
>
<Eye className="h-4 w-4" />
<Maximize2 className="h-4 w-4" />
</Button>
<Button
onClick={handleDownload}
size="sm"
variant="ghost"
className="text-white hover:bg-white/20"
title="Скачать изображение"
>
<Download className="h-4 w-4" />
</Button>
@ -104,31 +142,14 @@ export function ImageMessage({ imageUrl, fileName, fileSize, isCurrentUser = fal
</div>
</div>
{/* Полноэкранный просмотр */}
{showFullSize && (
<div
className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center p-4"
onClick={() => setShowFullSize(false)}
>
<div className="relative max-w-full max-h-full">
<Image
src={imageUrl}
alt={fileName}
width={800}
height={600}
className="max-w-full max-h-full object-contain"
onClick={(e) => e.stopPropagation()}
/>
<Button
onClick={() => setShowFullSize(false)}
className="absolute top-4 right-4 bg-black/50 hover:bg-black/70 text-white"
size="sm"
>
×
</Button>
</div>
</div>
)}
{/* Lightbox для полноэкранного просмотра */}
<ImageLightbox
imageUrl={imageUrl}
fileName={fileName}
fileSize={fileSize}
isOpen={isLightboxOpen}
onClose={handleCloseLightbox}
/>
</>
)
}

View File

@ -171,7 +171,7 @@ export function VoiceRecorder({ onSendVoice }: VoiceRecorderProps) {
}
return (
<div className="flex items-center space-x-2">
<>
{!recordedAudio ? (
// Состояние записи
<>
@ -179,9 +179,8 @@ export function VoiceRecorder({ onSendVoice }: VoiceRecorderProps) {
<Button
type="button"
variant="ghost"
size="sm"
onClick={startRecording}
className="text-white/60 hover:text-white hover:bg-white/10 p-2"
className="text-white/60 hover:text-white hover:bg-white/10 h-10 w-10 p-0"
>
<Mic className="h-4 w-4" />
</Button>
@ -249,6 +248,6 @@ export function VoiceRecorder({ onSendVoice }: VoiceRecorderProps) {
</Button>
</div>
)}
</div>
</>
)
}