Улучшены стили и функциональность компонентов мессенджера, включая поддержку многострочного ввода сообщений и обновление стилей кнопок. Добавлен новый метод скачивания файлов через API для изображений и документов с обработкой ошибок. Оптимизированы компоненты для загрузки файлов и записи голоса, улучшен интерфейс выбора эмодзи.
This commit is contained in:
@ -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>
|
||||
|
@ -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 (
|
||||
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
205
src/components/ui/image-lightbox.tsx
Normal file
205
src/components/ui/image-lightbox.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user