Files
sfera-new/src/components/messenger/messenger-attachments.tsx

406 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState, useEffect } from 'react'
import { useQuery } from '@apollo/client'
import { GET_MESSAGES } from '@/graphql/queries'
import { useAuth } from '@/hooks/useAuth'
import { Card } from '@/components/ui/card'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import { ImageLightbox } from '@/components/ui/image-lightbox'
import { VoicePlayer } from '@/components/ui/voice-player'
import {
FileText,
Image,
Music,
Video,
Download,
Calendar,
User,
RefreshCw
} from 'lucide-react'
interface Organization {
id: string
inn: string
name?: string
fullName?: string
type: 'FULFILLMENT' | 'SELLER' | 'LOGIST' | 'WHOLESALE'
users?: Array<{ id: string, avatar?: string, managerName?: string }>
}
interface Message {
id: string
content?: string
type?: 'TEXT' | 'VOICE' | 'IMAGE' | 'FILE'
voiceUrl?: string
voiceDuration?: number
fileUrl?: string
fileName?: string
fileSize?: number
fileType?: string
senderId: string
senderOrganization: Organization
receiverOrganization: Organization
createdAt: string
isRead: boolean
}
interface MessengerAttachmentsProps {
counterparty: Organization
onViewChange?: () => void
}
export function MessengerAttachments({ counterparty, onViewChange }: MessengerAttachmentsProps) {
const { user } = useAuth()
const [activeTab, setActiveTab] = useState('all')
const [lightboxImage, setLightboxImage] = useState<{ url: string; fileName: string; fileSize?: number } | null>(null)
// Загружаем все сообщения для получения вложений
const { data: messagesData, loading, refetch } = useQuery(GET_MESSAGES, {
variables: { counterpartyId: counterparty.id, limit: 1000 },
fetchPolicy: 'cache-and-network',
pollInterval: 5000, // Обновляем каждые 5 секунд
notifyOnNetworkStatusChange: false, // Не показываем loading при обновлениях
})
// Обновляем данные при открытии вкладки вложений
useEffect(() => {
onViewChange?.()
}, [onViewChange])
const messages: Message[] = messagesData?.messages || []
// Фильтруем только сообщения с вложениями
const attachmentMessages = messages.filter(msg =>
msg.type && ['VOICE', 'IMAGE', 'FILE'].includes(msg.type) &&
(msg.fileUrl || (msg.type === 'VOICE' && msg.voiceUrl))
)
// Группируем по типам
const imageMessages = attachmentMessages.filter(msg => msg.type === 'IMAGE')
const voiceMessages = attachmentMessages.filter(msg => msg.type === 'VOICE')
const fileMessages = attachmentMessages.filter(msg => msg.type === 'FILE')
const getOrganizationName = (org: Organization) => {
return org.name || org.fullName || 'Организация'
}
const getInitials = (org: Organization) => {
const name = getOrganizationName(org)
return name.charAt(0).toUpperCase()
}
const formatFileSize = (bytes?: number) => {
if (!bytes) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
const formatDate = (dateString: string) => {
const date = new Date(dateString)
return date.toLocaleDateString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
})
}
const formatTime = (dateString: string) => {
const date = new Date(dateString)
return date.toLocaleTimeString('ru-RU', {
hour: '2-digit',
minute: '2-digit'
})
}
const handleDownload = (fileUrl: string, fileName: string) => {
const link = document.createElement('a')
link.href = fileUrl
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
const handleImageClick = (imageUrl: string, fileName?: string, fileSize?: number) => {
setLightboxImage({
url: imageUrl,
fileName: fileName || 'Изображение',
fileSize
})
}
const renderFileIcon = (fileType?: string) => {
if (!fileType) return <FileText className="h-4 w-4 text-gray-400" />
if (fileType.startsWith('image/')) return <Image className="h-4 w-4 text-blue-400" aria-hidden="true" />
if (fileType.startsWith('audio/')) return <Music className="h-4 w-4 text-green-400" />
if (fileType.startsWith('video/')) return <Video className="h-4 w-4 text-purple-400" />
return <FileText className="h-4 w-4 text-gray-400" />
}
const renderAttachmentCard = (message: Message) => {
const isCurrentUser = message.senderOrganization?.id === user?.organization?.id
const senderOrg = isCurrentUser ? user?.organization : message.senderOrganization
return (
<Card key={message.id} className="glass-card p-4 mb-3">
<div className="flex items-start space-x-3">
{/* Аватар отправителя */}
<Avatar className="h-8 w-8 flex-shrink-0">
{(senderOrg as Organization)?.users?.[0]?.avatar ? (
<AvatarImage
src={(senderOrg as Organization).users![0].avatar!}
alt="Аватар отправителя"
className="w-full h-full object-cover"
/>
) : null}
<AvatarFallback className="bg-purple-500 text-white text-xs">
{getInitials(senderOrg as Organization)}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-2">
<span className="text-white/80 text-sm font-medium">
{isCurrentUser ? 'Вы' : getOrganizationName(senderOrg as Organization)}
</span>
<Badge variant="outline" className="text-xs">
{isCurrentUser ? 'Исходящий' : 'Входящий'}
</Badge>
</div>
<div className="flex items-center space-x-2 text-xs text-white/50">
<Calendar className="h-3 w-3" />
<span>{formatDate(message.createdAt)}</span>
<span>{formatTime(message.createdAt)}</span>
</div>
</div>
{/* Содержимое вложения */}
{message.type === 'VOICE' && message.voiceUrl ? (
/* Голосовой плеер */
<div className="w-full">
<VoicePlayer
audioUrl={message.voiceUrl}
duration={message.voiceDuration || 0}
isCurrentUser={isCurrentUser}
/>
</div>
) : (
/* Обычные файлы и изображения */
<div className="flex items-center justify-between bg-white/5 rounded-lg p-3">
<div className="flex items-center space-x-3 flex-1 min-w-0">
{/* Превью изображения или иконка */}
{message.type === 'IMAGE' && message.fileUrl ? (
<div
className="relative w-12 h-12 rounded-lg overflow-hidden cursor-pointer hover:opacity-80 transition-opacity"
onClick={() => handleImageClick(message.fileUrl!, message.fileName, message.fileSize)}
>
<img
src={message.fileUrl}
alt={message.fileName || 'Изображение'}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-black/20 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
<Image className="h-4 w-4 text-white" aria-hidden="true" />
</div>
</div>
) : (
<div className="flex items-center justify-center w-10 h-10 bg-white/10 rounded-lg flex-shrink-0">
{renderFileIcon(message.fileType)}
</div>
)}
<div className="flex-1 min-w-0">
<p className="text-white text-sm font-medium truncate">
{message.fileName ||
(message.type === 'IMAGE' ? 'Изображение' : 'Файл')}
</p>
<div className="flex items-center space-x-2 text-xs text-white/60">
{message.fileSize && (
<span>{formatFileSize(message.fileSize)}</span>
)}
{message.type === 'IMAGE' && (
<span className="text-blue-300"> Нажмите для просмотра</span>
)}
</div>
</div>
</div>
<button
onClick={() => handleDownload(message.fileUrl!, message.fileName || 'file')}
className="flex items-center justify-center w-8 h-8 bg-white/10 hover:bg-white/20 rounded-lg transition-colors flex-shrink-0 ml-2"
title="Скачать файл"
>
<Download className="h-4 w-4 text-white/70" />
</button>
</div>
)}
</div>
</div>
</Card>
)
}
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-white/60">Загрузка вложений...</div>
</div>
)
}
if (attachmentMessages.length === 0) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<FileText className="h-12 w-12 text-white/20 mx-auto mb-4" />
<p className="text-white/60 text-lg mb-2">Нет вложений</p>
<p className="text-white/40 text-sm">
Файлы, изображения и голосовые сообщения будут отображаться здесь
</p>
</div>
</div>
)
}
return (
<div className="h-full flex flex-col">
<div className="flex-1 overflow-auto p-4">
{/* Кнопка обновления */}
<div className="flex justify-between items-center mb-4">
<h2 className="text-white/80 text-lg font-medium">Вложения</h2>
<button
onClick={() => refetch()}
className="flex items-center space-x-2 px-3 py-1 bg-white/10 hover:bg-white/20 rounded-lg transition-colors"
title="Обновить вложения"
>
<RefreshCw className="h-4 w-4 text-white/70" />
<span className="text-white/70 text-sm">Обновить</span>
</button>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-4 mb-6">
<TabsTrigger value="all" className="text-xs">
Все ({attachmentMessages.length})
</TabsTrigger>
<TabsTrigger value="images" className="text-xs">
Фото ({imageMessages.length})
</TabsTrigger>
<TabsTrigger value="audio" className="text-xs">
Аудио ({voiceMessages.length})
</TabsTrigger>
<TabsTrigger value="files" className="text-xs">
Файлы ({fileMessages.length})
</TabsTrigger>
</TabsList>
<TabsContent value="all" className="mt-0">
{attachmentMessages.length === 0 ? (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<FileText className="h-12 w-12 text-white/20 mx-auto mb-4" />
<p className="text-white/60">Нет вложений</p>
</div>
</div>
) : (
<div>
{attachmentMessages.map(renderAttachmentCard)}
</div>
)}
</TabsContent>
<TabsContent value="images" className="mt-0">
{imageMessages.length === 0 ? (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<Image className="h-12 w-12 text-white/20 mx-auto mb-4" aria-hidden="true" />
<p className="text-white/60">Нет изображений</p>
</div>
</div>
) : (
<div>
{/* Сетка изображений для быстрого просмотра */}
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3 mb-6">
{imageMessages.map((msg) => (
<div
key={`grid-${msg.id}`}
className="relative aspect-square rounded-lg overflow-hidden cursor-pointer hover:opacity-80 transition-opacity group"
onClick={() => handleImageClick(msg.fileUrl!, msg.fileName, msg.fileSize)}
>
<img
src={msg.fileUrl!}
alt={msg.fileName || 'Изображение'}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors flex items-center justify-center">
<Image className="h-6 w-6 text-white opacity-0 group-hover:opacity-100 transition-opacity" aria-hidden="true" />
</div>
<div className="absolute bottom-2 left-2 right-2">
<div className="text-white text-xs truncate bg-black/50 px-2 py-1 rounded">
{formatDate(msg.createdAt)}
</div>
</div>
</div>
))}
</div>
{/* Детальный список карточек */}
<div className="border-t border-white/10 pt-4">
<h4 className="text-white/80 text-sm font-medium mb-3">Детальная информация</h4>
{imageMessages.map(renderAttachmentCard)}
</div>
</div>
)}
</TabsContent>
<TabsContent value="audio" className="mt-0">
{voiceMessages.length === 0 ? (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<Music className="h-12 w-12 text-white/20 mx-auto mb-4" />
<p className="text-white/60">Нет аудио записей</p>
</div>
</div>
) : (
<div>
{voiceMessages.map(renderAttachmentCard)}
</div>
)}
</TabsContent>
<TabsContent value="files" className="mt-0">
{fileMessages.length === 0 ? (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<FileText className="h-12 w-12 text-white/20 mx-auto mb-4" />
<p className="text-white/60">Нет файлов</p>
</div>
</div>
) : (
<div>
{fileMessages.map(renderAttachmentCard)}
</div>
)}
</TabsContent>
</Tabs>
</div>
{/* Lightbox для просмотра изображений */}
{lightboxImage && (
<ImageLightbox
imageUrl={lightboxImage.url}
fileName={lightboxImage.fileName}
fileSize={lightboxImage.fileSize}
isOpen={true}
onClose={() => setLightboxImage(null)}
/>
)}
</div>
)
}