Эксперимент: интеграция @chatscope/chat-ui-kit-react для мессенджера
- Установлены пакеты @chatscope/chat-ui-kit-react и @chatscope/chat-ui-kit-styles - Создана новая страница /messages-chatscope с полнофункциональным мессенджером - Добавлен пункт 'Мессенджер (NEW)' в навигационное меню - Реализована интеграция с существующим Django API - Добавлена поддержка WebSocket для real-time сообщений - Список чатов с поиском и аватарами - Отображение сообщений с датами и вложениями - Индикатор 'печатает...' через WebSocket
This commit is contained in:
parent
e6243e895c
commit
e328e31d32
91
frontend/package-lock.json
generated
91
frontend/package-lock.json
generated
@ -8,6 +8,8 @@
|
||||
"name": "corp.front",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@chatscope/chat-ui-kit-react": "^2.1.1",
|
||||
"@chatscope/chat-ui-kit-styles": "^1.4.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.574.0",
|
||||
"next": "16.1.6",
|
||||
@ -274,6 +276,32 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@chatscope/chat-ui-kit-react": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@chatscope/chat-ui-kit-react/-/chat-ui-kit-react-2.1.1.tgz",
|
||||
"integrity": "sha512-rCtE9abdmAbBDkAAUYBC1TDTBMZHquqFIZhADptAfHcJ8z8W3XH/z/ZuwBSJXtzi6h1mwCNc3tBmm1A2NLGhNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@chatscope/chat-ui-kit-styles": "^1.2.0",
|
||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"classnames": "^2.2.6",
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.12.0 || ^17.0.0 || ^18.2.0 || ^19.0.0",
|
||||
"react-dom": "^16.12.0 || ^17.0.0 || ^18.2.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@chatscope/chat-ui-kit-styles": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@chatscope/chat-ui-kit-styles/-/chat-ui-kit-styles-1.4.0.tgz",
|
||||
"integrity": "sha512-016mBJD3DESw7Nh+lkKcPd22xG92ghA0VpIXIbjQtmXhC7Ve6wRazTy8z1Ahut+Tbv179+JxrftuMngsj/yV8Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz",
|
||||
@ -445,6 +473,63 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
|
||||
"integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-free": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz",
|
||||
"integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==",
|
||||
"license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
|
||||
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
|
||||
"integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==",
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/react-fontawesome": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.6.tgz",
|
||||
"integrity": "sha512-mtBFIi1UsYQo7rYonYFkjgYKGoL8T+fEH6NGUpvuqtY3ytMsAoDaPo5rk25KuMtKDipY4bGYM/CkmCHA1N3FUg==",
|
||||
"deprecated": "v0.2.x is no longer supported. Unless you are still using FontAwesome 5, please update to v3.1.1 or greater.",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "~1 || ~6 || ~7",
|
||||
"react": "^16.3 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@ -2495,6 +2580,12 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/client-only": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||
|
||||
@ -10,6 +10,8 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chatscope/chat-ui-kit-react": "^2.1.1",
|
||||
"@chatscope/chat-ui-kit-styles": "^1.4.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.574.0",
|
||||
"next": "16.1.6",
|
||||
|
||||
426
frontend/src/app/messages-chatscope/page.tsx
Normal file
426
frontend/src/app/messages-chatscope/page.tsx
Normal file
@ -0,0 +1,426 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
MainContainer,
|
||||
Sidebar,
|
||||
Search,
|
||||
ConversationList,
|
||||
Conversation,
|
||||
Avatar,
|
||||
ChatContainer,
|
||||
ConversationHeader,
|
||||
MessageList,
|
||||
Message,
|
||||
MessageInput,
|
||||
MessageSeparator,
|
||||
TypingIndicator,
|
||||
} from '@chatscope/chat-ui-kit-react';
|
||||
import '@chatscope/chat-ui-kit-styles/dist/default/styles.min.css';
|
||||
import { AppShell } from '@/components/AppShell';
|
||||
import { apiClient } from '@/lib/api';
|
||||
import { useUser } from '@/contexts/UserContext';
|
||||
import type { Chat as ChatType, Message as MessageType } from '@/types/api';
|
||||
|
||||
const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || "https://corp.robotail.pro";
|
||||
|
||||
function resolveAvatarUrl(url?: string | null): string {
|
||||
if (!url) return "";
|
||||
if (url.startsWith("data:")) return url;
|
||||
if (/^https?:\/\//i.test(url)) return url;
|
||||
if (url.startsWith("//")) return `https:${url}`;
|
||||
if (url.startsWith("/") && BACKEND_URL) {
|
||||
return `${BACKEND_URL.replace(/\/$/, "")}${url}`;
|
||||
}
|
||||
if (BACKEND_URL) {
|
||||
return `${BACKEND_URL.replace(/\/$/, "")}/${url.replace(/^\/+/, "")}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function getChatTitle(chat: ChatType, currentUserId?: number): string {
|
||||
const chatKind = chat.chat_type || chat.type;
|
||||
const rawName = (chat.name || "").trim();
|
||||
|
||||
if (chatKind === "direct" || chatKind === "private" || !rawName || rawName.toLowerCase() === "диалог") {
|
||||
if (chat.interlocutor?.name?.trim()) {
|
||||
return chat.interlocutor.name.trim();
|
||||
}
|
||||
|
||||
const detailsOther = (chat.participant_details || []).find((p) => p.id !== currentUserId);
|
||||
if (detailsOther?.name?.trim()) {
|
||||
return detailsOther.name.trim();
|
||||
}
|
||||
|
||||
const participants = (chat.participants || []).filter(
|
||||
(p): p is Exclude<typeof p, number> => typeof p === "object" && p !== null
|
||||
);
|
||||
const other = participants.find((p) => p.id !== currentUserId);
|
||||
if (other && typeof other === "object") {
|
||||
const name = `${other.last_name || ""} ${other.first_name || ""}`.trim();
|
||||
if (name) return name;
|
||||
if (other.email) return other.email;
|
||||
}
|
||||
}
|
||||
|
||||
return rawName || "Диалог";
|
||||
}
|
||||
|
||||
function getChatAvatar(chat: ChatType, currentUserId?: number): string {
|
||||
const chatKind = chat.chat_type || chat.type;
|
||||
if (chatKind === "direct" || chatKind === "private") {
|
||||
if (chat.interlocutor?.avatar) return resolveAvatarUrl(chat.interlocutor.avatar);
|
||||
|
||||
const detailsOther = (chat.participant_details || []).find((p) => p.id !== currentUserId);
|
||||
if (detailsOther?.avatar) return resolveAvatarUrl(detailsOther.avatar);
|
||||
|
||||
const participants = (chat.participants || []).filter(
|
||||
(p): p is Exclude<typeof p, number> => typeof p === "object" && p !== null
|
||||
);
|
||||
const other = participants.find((p) => p.id !== currentUserId);
|
||||
if (other?.avatar) return resolveAvatarUrl(other.avatar);
|
||||
}
|
||||
return resolveAvatarUrl(chat.avatar);
|
||||
}
|
||||
|
||||
export default function ChatscapeMessenger() {
|
||||
const { user } = useUser();
|
||||
const currentUserId = user?.id;
|
||||
|
||||
const [chats, setChats] = useState<ChatType[]>([]);
|
||||
const [selectedChatId, setSelectedChatId] = useState<number | null>(null);
|
||||
const [messages, setMessages] = useState<MessageType[]>([]);
|
||||
const [search, setSearch] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [messagesLoading, setMessagesLoading] = useState(false);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [typing, setTyping] = useState(false);
|
||||
const [typingTimeout, setTypingTimeout] = useState<NodeJS.Timeout | null>(null);
|
||||
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
|
||||
// Загрузка списка чатов
|
||||
useEffect(() => {
|
||||
const loadChats = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await apiClient.getChats();
|
||||
const chatsList = response.results || [];
|
||||
setChats(chatsList);
|
||||
|
||||
// Автоматически открываем первый чат
|
||||
if (chatsList.length > 0 && !selectedChatId) {
|
||||
setSelectedChatId(chatsList[0].id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading chats:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadChats();
|
||||
}, []);
|
||||
|
||||
// Загрузка сообщений выбранного чата
|
||||
useEffect(() => {
|
||||
if (!selectedChatId) return;
|
||||
|
||||
const loadMessages = async () => {
|
||||
try {
|
||||
setMessagesLoading(true);
|
||||
const data = await apiClient.getChatMessages(selectedChatId, { limit: 50 });
|
||||
setMessages(data.messages || []);
|
||||
} catch (error) {
|
||||
console.error('Error loading messages:', error);
|
||||
} finally {
|
||||
setMessagesLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadMessages();
|
||||
}, [selectedChatId]);
|
||||
|
||||
// WebSocket для real-time обновлений
|
||||
useEffect(() => {
|
||||
if (!selectedChatId) return;
|
||||
|
||||
const ws = new WebSocket('ws://localhost:9000/ws/');
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('✅ WebSocket connected');
|
||||
// Открываем чат
|
||||
ws.send(JSON.stringify({
|
||||
action: 'open_chat',
|
||||
chat_id: selectedChatId,
|
||||
load_history: false
|
||||
}));
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('📨 WebSocket message:', data);
|
||||
|
||||
if (data.type === 'chat_message') {
|
||||
// Новое сообщение
|
||||
setMessages(prev => {
|
||||
// Проверяем, нет ли уже такого сообщения
|
||||
if (prev.some(m => m.id === data.payload.id)) {
|
||||
return prev;
|
||||
}
|
||||
return [...prev, data.payload];
|
||||
});
|
||||
} else if (data.type === 'chat_message_edited') {
|
||||
// Редактирование
|
||||
setMessages(prev =>
|
||||
prev.map(m => m.id === data.payload.id ? { ...m, ...data.payload } : m)
|
||||
);
|
||||
} else if (data.type === 'chat_message_deleted') {
|
||||
// Удаление
|
||||
setMessages(prev =>
|
||||
prev.filter(m => m.id !== data.message_id)
|
||||
);
|
||||
} else if (data.type === 'chat_user_typing') {
|
||||
// Показываем "печатает..."
|
||||
if (data.user_id !== currentUserId) {
|
||||
setTyping(true);
|
||||
if (typingTimeout) clearTimeout(typingTimeout);
|
||||
const timeout = setTimeout(() => setTyping(false), 3000);
|
||||
setTypingTimeout(timeout);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('❌ WebSocket error:', error);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('🔌 WebSocket disconnected');
|
||||
};
|
||||
|
||||
return () => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({
|
||||
action: 'close_chat',
|
||||
chat_id: selectedChatId
|
||||
}));
|
||||
}
|
||||
ws.close();
|
||||
if (typingTimeout) clearTimeout(typingTimeout);
|
||||
};
|
||||
}, [selectedChatId, currentUserId]);
|
||||
|
||||
// Отправка индикатора печати
|
||||
const handleTyping = () => {
|
||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||
wsRef.current.send(JSON.stringify({ action: 'typing' }));
|
||||
}
|
||||
};
|
||||
|
||||
// Отправка сообщения
|
||||
const handleSend = async (text: string) => {
|
||||
if (!selectedChatId || !text.trim() || sending) return;
|
||||
|
||||
try {
|
||||
setSending(true);
|
||||
|
||||
// Отправляем через REST API
|
||||
await apiClient.sendMessage(selectedChatId, text);
|
||||
|
||||
// WebSocket автоматически добавит сообщение через chat_message event
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error);
|
||||
alert('Ошибка отправки сообщения');
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Группировка сообщений по датам
|
||||
const groupedMessages = messages.reduce((acc, msg, index) => {
|
||||
const date = new Date(msg.created_at || '').toLocaleDateString('ru-RU', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
});
|
||||
const prevDate = index > 0
|
||||
? new Date(messages[index - 1].created_at || '').toLocaleDateString('ru-RU', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
})
|
||||
: null;
|
||||
|
||||
if (date !== prevDate) {
|
||||
acc.push({ type: 'separator', date });
|
||||
}
|
||||
acc.push({ type: 'message', data: msg });
|
||||
|
||||
return acc;
|
||||
}, [] as Array<{ type: 'separator' | 'message'; date?: string; data?: MessageType }>);
|
||||
|
||||
const selectedChat = chats.find(c => c.id === selectedChatId);
|
||||
|
||||
const filteredChats = search
|
||||
? chats.filter(c => {
|
||||
const title = getChatTitle(c, currentUserId).toLowerCase();
|
||||
return title.includes(search.toLowerCase());
|
||||
})
|
||||
: chats;
|
||||
|
||||
return (
|
||||
<AppShell>
|
||||
<div className="rounded-2xl bg-white shadow-sm ring-1 ring-gray-100 overflow-hidden" style={{ height: '80vh' }}>
|
||||
<div className="mb-4">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Мессенджер (Chatscope Experiment)</h1>
|
||||
<p className="text-sm text-gray-600 mt-1">Экспериментальная версия с @chatscope/chat-ui-kit-react</p>
|
||||
</div>
|
||||
|
||||
<MainContainer responsive style={{ height: 'calc(100% - 60px)' }}>
|
||||
{/* Боковая панель со списком чатов */}
|
||||
<Sidebar position="left" scrollable={false}>
|
||||
<Search
|
||||
placeholder="Поиск чатов..."
|
||||
value={search}
|
||||
onChange={(v) => setSearch(v)}
|
||||
/>
|
||||
|
||||
<ConversationList loading={loading}>
|
||||
{filteredChats.map(chat => {
|
||||
const title = getChatTitle(chat, currentUserId);
|
||||
const avatar = getChatAvatar(chat, currentUserId);
|
||||
const lastMessage = chat.last_message?.content || 'Нет сообщений';
|
||||
const unreadCount = chat.unread_count || 0;
|
||||
|
||||
return (
|
||||
<Conversation
|
||||
key={chat.id}
|
||||
name={title}
|
||||
lastSenderName={chat.last_message?.author_name}
|
||||
info={lastMessage.length > 50 ? lastMessage.slice(0, 50) + '...' : lastMessage}
|
||||
active={chat.id === selectedChatId}
|
||||
unreadCnt={unreadCount}
|
||||
onClick={() => setSelectedChatId(chat.id)}
|
||||
>
|
||||
<Avatar
|
||||
src={avatar}
|
||||
name={title}
|
||||
/>
|
||||
</Conversation>
|
||||
);
|
||||
})}
|
||||
</ConversationList>
|
||||
</Sidebar>
|
||||
|
||||
{/* Основной чат */}
|
||||
{selectedChat ? (
|
||||
<ChatContainer>
|
||||
<ConversationHeader>
|
||||
<Avatar
|
||||
src={getChatAvatar(selectedChat, currentUserId)}
|
||||
name={getChatTitle(selectedChat, currentUserId)}
|
||||
/>
|
||||
<ConversationHeader.Content
|
||||
userName={getChatTitle(selectedChat, currentUserId)}
|
||||
info={selectedChat.participant_details?.length
|
||||
? `${selectedChat.participant_details.length} участников`
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
</ConversationHeader>
|
||||
|
||||
<MessageList
|
||||
loading={messagesLoading}
|
||||
typingIndicator={typing ? <TypingIndicator content="Печатает..." /> : undefined}
|
||||
>
|
||||
{groupedMessages.map((item, index) => {
|
||||
if (item.type === 'separator') {
|
||||
return (
|
||||
<MessageSeparator key={`sep-${index}`}>
|
||||
{item.date}
|
||||
</MessageSeparator>
|
||||
);
|
||||
}
|
||||
|
||||
const msg = item.data!;
|
||||
const isOwn = msg.author_id === currentUserId;
|
||||
|
||||
return (
|
||||
<Message
|
||||
key={msg.id}
|
||||
model={{
|
||||
message: msg.content || '',
|
||||
sender: msg.author_name || 'Неизвестно',
|
||||
direction: isOwn ? 'outgoing' : 'incoming',
|
||||
position: 'single'
|
||||
}}
|
||||
>
|
||||
{!isOwn && msg.author && (
|
||||
<Avatar
|
||||
src={resolveAvatarUrl(msg.author.avatar)}
|
||||
name={msg.author_name || ''}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Вложения */}
|
||||
{msg.attachments && msg.attachments.length > 0 && (
|
||||
<Message.CustomContent>
|
||||
<div className="mt-2 space-y-2">
|
||||
{msg.attachments.map((att) => {
|
||||
const isImage = att.file_type === 'image' || att.mime_type?.startsWith('image/');
|
||||
return (
|
||||
<div key={att.id}>
|
||||
{isImage ? (
|
||||
<img
|
||||
src={resolveAvatarUrl(att.file_url)}
|
||||
alt={att.file_name}
|
||||
className="max-w-xs rounded shadow"
|
||||
/>
|
||||
) : (
|
||||
<a
|
||||
href={resolveAvatarUrl(att.file_url)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 text-sky-600 hover:underline text-sm"
|
||||
>
|
||||
📎 {att.file_name}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Message.CustomContent>
|
||||
)}
|
||||
</Message>
|
||||
);
|
||||
})}
|
||||
</MessageList>
|
||||
|
||||
<MessageInput
|
||||
placeholder="Введите сообщение..."
|
||||
onSend={handleSend}
|
||||
onChange={handleTyping}
|
||||
disabled={sending}
|
||||
attachButton={false}
|
||||
/>
|
||||
</ChatContainer>
|
||||
) : (
|
||||
<ChatContainer>
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center text-gray-500">
|
||||
<p className="text-lg font-medium">Выберите чат</p>
|
||||
<p className="text-sm mt-1">Выберите диалог из списка слева</p>
|
||||
</div>
|
||||
</div>
|
||||
</ChatContainer>
|
||||
)}
|
||||
</MainContainer>
|
||||
</div>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Bell, Building2, CalendarDays, FileSignature, FileText, Home as HomeIcon, Menu, MessageSquare, Search, Users, Wallet, X } from "lucide-react";
|
||||
import { Bell, Building2, CalendarDays, FileSignature, FileText, Home as HomeIcon, Menu, MessageSquare, Search, Users, Wallet, X, Sparkles } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { ReactNode, useEffect, useState, useCallback } from "react";
|
||||
@ -38,6 +38,7 @@ type LeftNavContentProps = {
|
||||
const navItems = [
|
||||
{ href: "/", label: "Лента", icon: HomeIcon },
|
||||
{ href: "/messages", label: "Сообщения", icon: MessageSquare },
|
||||
{ href: "/messages-chatscope", label: "Мессенджер (NEW)", icon: Sparkles },
|
||||
{ href: "/calendar", label: "Календарь", icon: CalendarDays },
|
||||
{ href: "/employees", label: "Сотрудники", icon: Users },
|
||||
{ href: "/departments", label: "Отделы", icon: Building2 },
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user