Обновлен компонент MessengerAttachments: добавлена поддержка голосовых сообщений с использованием VoicePlayer, улучшена логика фильтрации вложений, добавлены функции обновления данных при открытии вкладки вложений. Удален устаревший компонент MessengerChatWithAttachments и обновлен интерфейс для улучшения взаимодействия с пользователем.

This commit is contained in:
Bivekich
2025-07-21 15:11:52 +03:00
parent b935807cc2
commit 8063ca64b8
4 changed files with 393 additions and 893 deletions

View File

@ -1,6 +1,6 @@
"use client"
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { useQuery } from '@apollo/client'
import { GET_MESSAGES } from '@/graphql/queries'
import { useAuth } from '@/hooks/useAuth'
@ -9,6 +9,7 @@ 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,
@ -16,7 +17,8 @@ import {
Video,
Download,
Calendar,
User
User,
RefreshCw
} from 'lucide-react'
interface Organization {
@ -47,24 +49,33 @@ interface Message {
interface MessengerAttachmentsProps {
counterparty: Organization
onViewChange?: () => void
}
export function MessengerAttachments({ counterparty }: MessengerAttachmentsProps) {
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 } = useQuery(GET_MESSAGES, {
variables: { counterpartyId: counterparty.id, limit: 1000 }, // Увеличиваем лимит для получения всех файлов
fetchPolicy: 'cache-first',
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', 'IMAGE', 'FILE'].includes(msg.type) &&
(msg.fileUrl || (msg.type === 'VOICE' && msg.voiceUrl))
)
// Группируем по типам
@ -76,10 +87,6 @@ export function MessengerAttachments({ counterparty }: MessengerAttachmentsProps
return org.name || org.fullName || 'Организация'
}
const getManagerName = (org: Organization) => {
return org.users?.[0]?.managerName || 'Управляющий'
}
const getInitials = (org: Organization) => {
const name = getOrganizationName(org)
return name.charAt(0).toUpperCase()
@ -128,12 +135,12 @@ export function MessengerAttachments({ counterparty }: MessengerAttachmentsProps
}
const renderFileIcon = (fileType?: string) => {
if (!fileType) return <FileText className="h-4 w-4" />
if (!fileType) return <FileText className="h-4 w-4 text-gray-400" />
if (fileType.startsWith('image/')) return <Image className="h-4 w-4" />
if (fileType.startsWith('audio/')) return <Music className="h-4 w-4" />
if (fileType.startsWith('video/')) return <Video className="h-4 w-4" />
return <FileText className="h-4 w-4" />
if (fileType.startsWith('image/')) return <Image className="h-4 w-4 text-blue-400" />
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) => {
@ -145,13 +152,13 @@ export function MessengerAttachments({ counterparty }: MessengerAttachmentsProps
<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}
{(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>
@ -175,61 +182,65 @@ export function MessengerAttachments({ counterparty }: MessengerAttachmentsProps
</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" />
{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" />
</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 className="flex items-center justify-center w-10 h-10 bg-white/10 rounded-lg flex-shrink-0">
{message.type === 'VOICE' ? (
<Music className="h-5 w-5 text-green-400" />
) : (
renderFileIcon(message.fileType)
)}
</div>
)}
<div className="flex-1 min-w-0">
<p className="text-white text-sm font-medium truncate">
{message.fileName ||
(message.type === 'VOICE' ? 'Голосовое сообщение' :
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.voiceDuration && (
<span>{Math.floor(message.voiceDuration / 60)}:{(message.voiceDuration % 60).toString().padStart(2, '0')}</span>
)}
{message.type === 'IMAGE' && (
<span className="text-blue-300"> Нажмите для просмотра</span>
)}
</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>
<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>
@ -244,131 +255,141 @@ export function MessengerAttachments({ counterparty }: MessengerAttachmentsProps
)
}
return (
<div className="h-full flex flex-col">
<div className="flex items-center space-x-3 mb-4 px-4 pt-4">
<FileText className="h-5 w-5 text-blue-400" />
<div>
<h3 className="text-lg font-semibold text-white">Вложения</h3>
<p className="text-white/60 text-sm">
{attachmentMessages.length} файлов от {getOrganizationName(counterparty)}
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>
)
}
<div className="flex-1 px-4 pb-4">
<Tabs value={activeTab} onValueChange={setActiveTab} className="h-full">
<TabsList className="grid w-full grid-cols-4 mb-4">
<TabsTrigger value="all" className="flex items-center space-x-2">
<FileText className="h-4 w-4" />
<span>Все ({attachmentMessages.length})</span>
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="flex items-center space-x-2">
<Image className="h-4 w-4" />
<span>Фото ({imageMessages.length})</span>
<TabsTrigger value="images" className="text-xs">
Фото ({imageMessages.length})
</TabsTrigger>
<TabsTrigger value="voice" className="flex items-center space-x-2">
<Music className="h-4 w-4" />
<span>Аудио ({voiceMessages.length})</span>
<TabsTrigger value="audio" className="text-xs">
Аудио ({voiceMessages.length})
</TabsTrigger>
<TabsTrigger value="files" className="flex items-center space-x-2">
<FileText className="h-4 w-4" />
<span>Файлы ({fileMessages.length})</span>
<TabsTrigger value="files" className="text-xs">
Файлы ({fileMessages.length})
</TabsTrigger>
</TabsList>
<div className="h-full overflow-auto">
<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>
<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>
{attachmentMessages.map(renderAttachmentCard)}
</div>
)}
</TabsContent>
</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" />
<p className="text-white/60">Нет изображений</p>
</div>
<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" />
<p className="text-white/60">Нет изображений</p>
</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" />
</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 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" />
</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 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>
))}
</div>
)}
</TabsContent>
{/* Детальный список карточек */}
<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="voice" 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>
<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>
{voiceMessages.map(renderAttachmentCard)}
</div>
)}
</TabsContent>
</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>
<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>
{fileMessages.map(renderAttachmentCard)}
</div>
)}
</TabsContent>
</div>
</div>
) : (
<div>
{fileMessages.map(renderAttachmentCard)}
</div>
)}
</TabsContent>
</Tabs>
</div>
</div>
{/* Lightbox для просмотра изображений */}
{lightboxImage && (