Улучшены стили и функциональность компонентов мессенджера, включая поддержку многострочного ввода сообщений и обновление стилей кнопок. Добавлен новый метод скачивания файлов через API для изображений и документов с обработкой ошибок. Оптимизированы компоненты для загрузки файлов и записи голоса, улучшен интерфейс выбора эмодзи.
This commit is contained in:
64
src/app/api/download-file/route.ts
Normal file
64
src/app/api/download-file/route.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const searchParams = request.nextUrl.searchParams
|
||||||
|
const fileUrl = searchParams.get('url')
|
||||||
|
const fileName = searchParams.get('filename')
|
||||||
|
|
||||||
|
if (!fileUrl) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'File URL is required' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что URL принадлежит нашему S3 хранилищу
|
||||||
|
if (!fileUrl.includes('s3.twcstorage.ru/617774af-sfera/')) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Invalid file URL' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔽 Проксируем скачивание файла:', fileUrl)
|
||||||
|
|
||||||
|
// Загружаем файл с S3
|
||||||
|
const response = await fetch(fileUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (compatible; SferaApp/1.0)',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('❌ Ошибка загрузки с S3:', response.status, response.statusText)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: `Failed to fetch file: ${response.status}` },
|
||||||
|
{ status: response.status }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем содержимое файла
|
||||||
|
const buffer = await response.arrayBuffer()
|
||||||
|
|
||||||
|
console.log('✅ Файл успешно загружен с S3, размер:', buffer.byteLength)
|
||||||
|
|
||||||
|
// Возвращаем файл с правильными заголовками для скачивания
|
||||||
|
return new NextResponse(buffer, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/octet-stream', // Принудительное скачивание
|
||||||
|
'Content-Disposition': `attachment; filename="${fileName || 'file'}"`,
|
||||||
|
'Content-Length': buffer.byteLength.toString(),
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка в download-file API:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to download file' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -192,14 +192,18 @@
|
|||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-input:focus {
|
.glass-input:focus,
|
||||||
|
.glass-input:focus-visible {
|
||||||
background: rgba(255, 255, 255, 0.12);
|
background: rgba(255, 255, 255, 0.12);
|
||||||
border: 1px solid rgba(168, 85, 247, 0.5);
|
border: 1px solid rgba(168, 85, 247, 0.6);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 0 3px rgba(168, 85, 247, 0.15),
|
0 0 0 3px rgba(168, 85, 247, 0.2),
|
||||||
0 4px 16px rgba(147, 51, 234, 0.25);
|
0 4px 20px rgba(147, 51, 234, 0.3),
|
||||||
|
0 0 20px rgba(168, 85, 247, 0.15);
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-button {
|
.glass-button {
|
||||||
|
@ -360,7 +360,7 @@ export function MessengerChat({ counterparty }: MessengerChatProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`flex ${isCurrentUser ? 'justify-end' : 'justify-start'} mb-3`}>
|
<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 && (
|
{!isCurrentUser && (
|
||||||
<div className="flex items-center space-x-2 mb-1 px-1">
|
<div className="flex items-center space-x-2 mb-1 px-1">
|
||||||
@ -409,12 +409,12 @@ export function MessengerChat({ counterparty }: MessengerChatProps) {
|
|||||||
isCurrentUser={isCurrentUser}
|
isCurrentUser={isCurrentUser}
|
||||||
/>
|
/>
|
||||||
) : msg.content ? (
|
) : msg.content ? (
|
||||||
<div className={`px-4 py-2 rounded-lg ${
|
<div className={`px-4 py-2 rounded-lg break-words ${
|
||||||
isCurrentUser
|
isCurrentUser
|
||||||
? 'bg-blue-500/20 text-white border border-blue-500/30'
|
? 'bg-blue-500/20 text-white border border-blue-500/30'
|
||||||
: 'bg-white/10 text-white border border-white/20'
|
: '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>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@ -434,28 +434,42 @@ export function MessengerChat({ counterparty }: MessengerChatProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Поле ввода сообщения */}
|
{/* Поле ввода сообщения */}
|
||||||
<div className="p-4 border-t border-white/10">
|
<div className="px-4 py-3 border-t border-white/10">
|
||||||
<div className="flex space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="flex flex-1 space-x-2">
|
<div className="flex-1">
|
||||||
<Input
|
<textarea
|
||||||
value={message}
|
value={message}
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
onKeyPress={handleKeyPress}
|
onKeyPress={handleKeyPress}
|
||||||
placeholder="Введите сообщение..."
|
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} />
|
<EmojiPickerComponent onEmojiSelect={handleEmojiSelect} />
|
||||||
<FileUploader onSendFile={handleSendFile} />
|
<FileUploader onSendFile={handleSendFile} />
|
||||||
<VoiceRecorder onSendVoice={handleSendVoice} />
|
<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>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,9 +45,8 @@ export function EmojiPickerComponent({ onEmojiSelect }: EmojiPickerComponentProp
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
|
||||||
onClick={() => setShowPicker(!showPicker)}
|
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" />
|
<Smile className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -39,13 +39,43 @@ export function FileMessage({ fileUrl, fileName, fileSize, fileType, isCurrentUs
|
|||||||
return parts.length > 1 ? parts.pop()?.toUpperCase() : 'FILE'
|
return parts.length > 1 ? parts.pop()?.toUpperCase() : 'FILE'
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDownload = () => {
|
const handleDownload = async () => {
|
||||||
const link = document.createElement('a')
|
console.log('🔽 Начинаем скачивание файла:', fileName, fileUrl)
|
||||||
link.href = fileUrl
|
|
||||||
link.download = fileName
|
try {
|
||||||
document.body.appendChild(link)
|
// Используем наш API endpoint для проксирования скачивания
|
||||||
link.click()
|
const downloadUrl = `/api/download-file?url=${encodeURIComponent(fileUrl)}&filename=${encodeURIComponent(fileName)}`
|
||||||
document.body.removeChild(link)
|
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 (
|
return (
|
||||||
|
@ -167,15 +167,14 @@ export function FileUploader({ onSendFile }: FileUploaderProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-2">
|
<>
|
||||||
{/* Кнопка для загрузки изображений */}
|
{/* Кнопка для загрузки изображений */}
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
|
||||||
onClick={() => imageInputRef.current?.click()}
|
onClick={() => imageInputRef.current?.click()}
|
||||||
disabled={isUploading}
|
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" />
|
<Image className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -184,10 +183,9 @@ export function FileUploader({ onSendFile }: FileUploaderProps) {
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
disabled={isUploading}
|
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" />
|
<Paperclip className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -214,6 +212,6 @@ export function FileUploader({ onSendFile }: FileUploaderProps) {
|
|||||||
Загрузка...
|
Загрузка...
|
||||||
</div>
|
</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 { useState } from 'react'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { Download, Eye } from 'lucide-react'
|
import { Download, Maximize2 } from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { ImageLightbox } from '@/components/ui/image-lightbox'
|
||||||
|
|
||||||
interface ImageMessageProps {
|
interface ImageMessageProps {
|
||||||
imageUrl: string
|
imageUrl: string
|
||||||
@ -14,7 +15,7 @@ interface ImageMessageProps {
|
|||||||
|
|
||||||
export function ImageMessage({ imageUrl, fileName, fileSize, isCurrentUser = false }: ImageMessageProps) {
|
export function ImageMessage({ imageUrl, fileName, fileSize, isCurrentUser = false }: ImageMessageProps) {
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [showFullSize, setShowFullSize] = useState(false)
|
const [isLightboxOpen, setIsLightboxOpen] = useState(false)
|
||||||
|
|
||||||
const formatFileSize = (bytes?: number) => {
|
const formatFileSize = (bytes?: number) => {
|
||||||
if (!bytes) return ''
|
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]
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDownload = () => {
|
const handleDownload = async (e: React.MouseEvent) => {
|
||||||
const link = document.createElement('a')
|
e.stopPropagation()
|
||||||
link.href = imageUrl
|
console.log('🔽 Начинаем скачивание изображения:', fileName, imageUrl)
|
||||||
link.download = fileName
|
|
||||||
document.body.appendChild(link)
|
try {
|
||||||
link.click()
|
// Используем наш API endpoint для проксирования скачивания
|
||||||
document.body.removeChild(link)
|
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 = () => {
|
const handleOpenLightbox = () => {
|
||||||
setShowFullSize(true)
|
setIsLightboxOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCloseLightbox = () => {
|
||||||
|
setIsLightboxOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -57,7 +93,7 @@ export function ImageMessage({ imageUrl, fileName, fileSize, isCurrentUser = fal
|
|||||||
objectFit: 'cover'
|
objectFit: 'cover'
|
||||||
}}
|
}}
|
||||||
onLoad={() => setIsLoading(false)}
|
onLoad={() => setIsLoading(false)}
|
||||||
onClick={handleImageClick}
|
onClick={handleOpenLightbox}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
@ -69,18 +105,20 @@ export function ImageMessage({ imageUrl, fileName, fileSize, isCurrentUser = fal
|
|||||||
{/* Overlay с кнопками при наведении */}
|
{/* 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">
|
<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
|
<Button
|
||||||
onClick={handleImageClick}
|
onClick={handleOpenLightbox}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="text-white hover:bg-white/20"
|
className="text-white hover:bg-white/20"
|
||||||
|
title="Открыть в полном размере"
|
||||||
>
|
>
|
||||||
<Eye className="h-4 w-4" />
|
<Maximize2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="text-white hover:bg-white/20"
|
className="text-white hover:bg-white/20"
|
||||||
|
title="Скачать изображение"
|
||||||
>
|
>
|
||||||
<Download className="h-4 w-4" />
|
<Download className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -104,31 +142,14 @@ export function ImageMessage({ imageUrl, fileName, fileSize, isCurrentUser = fal
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Полноэкранный просмотр */}
|
{/* Lightbox для полноэкранного просмотра */}
|
||||||
{showFullSize && (
|
<ImageLightbox
|
||||||
<div
|
imageUrl={imageUrl}
|
||||||
className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center p-4"
|
fileName={fileName}
|
||||||
onClick={() => setShowFullSize(false)}
|
fileSize={fileSize}
|
||||||
>
|
isOpen={isLightboxOpen}
|
||||||
<div className="relative max-w-full max-h-full">
|
onClose={handleCloseLightbox}
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -171,7 +171,7 @@ export function VoiceRecorder({ onSendVoice }: VoiceRecorderProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-2">
|
<>
|
||||||
{!recordedAudio ? (
|
{!recordedAudio ? (
|
||||||
// Состояние записи
|
// Состояние записи
|
||||||
<>
|
<>
|
||||||
@ -179,9 +179,8 @@ export function VoiceRecorder({ onSendVoice }: VoiceRecorderProps) {
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
|
||||||
onClick={startRecording}
|
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" />
|
<Mic className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -249,6 +248,6 @@ export function VoiceRecorder({ onSendVoice }: VoiceRecorderProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
Reference in New Issue
Block a user