# СИСТЕМА СООБЩЕНИЙ И КОММУНИКАЦИЙ ## 🎯 ОБЗОР СИСТЕМЫ Система сообщений SFERA обеспечивает многоканальную коммуникацию между организациями различных типов. Поддерживает текстовые сообщения, голосовые записи, изображения и файловые вложения с real-time доставкой через GraphQL subscriptions. ## 📊 МОДЕЛЬ ДАННЫХ ### Модель Message (Сообщение) ```typescript // Prisma модель Message - центральная сущность чата model Message { id String @id @default(cuid()) content String? // Текстовое содержимое type MessageType @default(TEXT) // Тип сообщения // Голосовые сообщения voiceUrl String? // URL аудиозаписи voiceDuration Int? // Длительность в секундах // Файловые вложения fileUrl String? // URL файла fileName String? // Исходное название файла fileSize Int? // Размер в байтах fileType String? // MIME тип файла // Статус прочтения isRead Boolean @default(false) // Связи участников (B2B коммуникация) senderId String // ID пользователя-отправителя senderOrganizationId String // ID организации-отправителя receiverOrganizationId String // ID организации-получателя // Relations sender User @relation("SentMessages", fields: [senderId], references: [id]) senderOrganization Organization @relation("SentMessages", fields: [senderOrganizationId], references: [id]) receiverOrganization Organization @relation("ReceivedMessages", fields: [receiverOrganizationId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Индексы для производительности @@index([senderOrganizationId, receiverOrganizationId, createdAt]) @@index([receiverOrganizationId, isRead]) } ``` ### Типы сообщений ```typescript // Поддерживаемые типы контента enum MessageType { TEXT // 📝 Текстовое сообщение VOICE // 🎤 Голосовая запись IMAGE // 🖼️ Изображение FILE // 📎 Файловое вложение } ``` ## 🏗️ АРХИТЕКТУРА КОМПОНЕНТОВ ### Структура мессенджера ``` src/components/messenger/ ├── messenger-dashboard.tsx # 🎯 Главная панель мессенджера ├── messenger-conversations.tsx # 💬 Список бесед ├── messenger-chat.tsx # 📱 Интерфейс чата ├── messenger-attachments.tsx # 📎 Обработка вложений └── messenger-empty-state.tsx # 🚫 Пустое состояние src/components/ui/ (специализированные для сообщений) ├── voice-recorder.tsx # 🎤 Запись голосовых сообщений ├── voice-player.tsx # 🔊 Проигрывание аудио ├── file-message.tsx # 📄 Отображение файлов ├── image-message.tsx # 🖼️ Галерея изображений ├── emoji-picker.tsx # 😊 Выбор эмодзи └── file-uploader.tsx # ⬆️ Загрузка файлов ``` ### Главная панель мессенджера ```typescript // MessengerDashboard - резизабельная панельная архитектура const MessengerDashboard = () => { const [selectedCounterparty, setSelectedCounterparty] = useState(null) // Real-time подключение для уведомлений const { unreadCount } = useRealtime({ channel: 'messages', organizationId: user.organizationId }) // Запрос списка диалогов const { data: conversations, refetch } = useQuery(GET_CONVERSATIONS, { pollInterval: 30000 // Обновление каждые 30 секунд }) return (
{/* Левая панель - список бесед */}

Сообщения {unreadCount > 0 && ( {unreadCount} )}

{/* Правая панель - активный чат */} {selectedCounterparty ? ( setSelectedCounterparty(null)} /> ) : ( )}
) } ``` ## 💬 ИНТЕРФЕЙС ЧАТА ### Компонент чата ```typescript // MessengerChat - основной интерфейс переписки const MessengerChat = ({ counterpartyId, onBack }: MessengerChatProps) => { const [messageText, setMessageText] = useState('') const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false) const messagesEndRef = useRef(null) // Мутации для отправки разных типов сообщений const [sendMessage] = useMutation(SEND_MESSAGE) const [sendVoiceMessage] = useMutation(SEND_VOICE_MESSAGE) const [sendImageMessage] = useMutation(SEND_IMAGE_MESSAGE) const [sendFileMessage] = useMutation(SEND_FILE_MESSAGE) const [markAsRead] = useMutation(MARK_MESSAGES_AS_READ) // Загрузка истории сообщений const { data: messagesData, subscribeToMore } = useQuery(GET_MESSAGES, { variables: { counterpartyId }, onCompleted: () => { scrollToBottom() markUnreadAsRead() } }) // Real-time подписка на новые сообщения useEffect(() => { const unsubscribe = subscribeToMore({ document: MESSAGE_SUBSCRIPTION, variables: { organizationIds: [user.organizationId, counterpartyId] }, updateQuery: (prev, { subscriptionData }) => { if (!subscriptionData.data) return prev const newMessage = subscriptionData.data.messageUpdated return { ...prev, messages: [...prev.messages, newMessage] } } }) return unsubscribe }, [counterpartyId]) // Отправка текстового сообщения const handleSendMessage = async () => { if (!messageText.trim()) return try { await sendMessage({ variables: { receiverOrganizationId: counterpartyId, content: messageText.trim(), type: 'TEXT' } }) setMessageText('') scrollToBottom() } catch (error) { toast.error('Ошибка отправки сообщения') } } // Обработка голосового сообщения const handleSendVoice = async (audioUrl: string, duration: number) => { try { await sendVoiceMessage({ variables: { receiverOrganizationId: counterpartyId, voiceUrl: audioUrl, voiceDuration: duration } }) scrollToBottom() } catch (error) { toast.error('Ошибка отправки голосового сообщения') } } return (
{/* Заголовок чата */}
{getInitials(counterparty?.name || counterparty?.fullName)}

{counterparty?.name || counterparty?.fullName}

{getOrganizationTypeLabel(counterparty?.type)}

{/* История сообщений */}
{messagesData?.messages?.map((message) => ( ))}
{/* Поле ввода */}
{/* Вложения */} handleSendImage(fileUrl, fileName, fileSize, fileType) } onSendFile={(fileUrl, fileName, fileSize, fileType) => handleSendFile(fileUrl, fileName, fileSize, fileType) } /> {/* Голосовая запись */} {/* Текстовое поле */}