Улучшены стили и функциональность компонентов мессенджера, включая поддержку многострочного ввода сообщений и обновление стилей кнопок. Добавлен новый метод скачивания файлов через 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);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
transition: all 0.3s ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.glass-input:focus {
|
||||
.glass-input:focus,
|
||||
.glass-input:focus-visible {
|
||||
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:
|
||||
0 0 0 3px rgba(168, 85, 247, 0.15),
|
||||
0 4px 16px rgba(147, 51, 234, 0.25);
|
||||
0 0 0 3px rgba(168, 85, 247, 0.2),
|
||||
0 4px 20px rgba(147, 51, 234, 0.3),
|
||||
0 0 20px rgba(168, 85, 247, 0.15);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.glass-button {
|
||||
|
@ -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>
|
||||
|
@ -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