Files
janichat/frontend/src/pages/MainLayout.tsx

207 lines
8.2 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.
import { useState, useEffect } from 'react';
import { Search, Edit, LogOut, Settings, Shield, Wifi, WifiOff } from 'lucide-react';
import { useStore } from '../store';
import { useNavigate } from 'react-router-dom';
import { wsClient } from '../api/ws';
import api from '../api/client';
import Avatar from '../components/Avatar';
import ChatListItem from '../components/ChatListItem';
import MessageList from '../components/MessageList';
import ChatHeader from '../components/ChatHeader';
import ChatInfoPanel from '../components/ChatInfoPanel';
import NewChatModal from '../components/NewChatModal';
import AdminPanel from '../components/admin/AdminPanel';
import { usePushNotifications } from '../hooks/usePushNotifications';
import { Chat } from '../types';
export default function MainLayout() {
const { user, chats, activeChat, setActiveChat, connected, logout, onlineUsers, updateChat } = useStore();
const navigate = useNavigate();
const [search, setSearch] = useState('');
const [showNew, setShowNew] = useState(false);
const [showAdmin, setShowAdmin] = useState(false);
const [showInfo, setShowInfo] = useState(false);
const [mobileChatOpen, setMobileChatOpen] = useState(false);
const [fullChat, setFullChat] = useState<Chat | null>(null);
usePushNotifications(!!user);
// Handle URL param ?chat=xxx
useEffect(() => {
const params = new URLSearchParams(location.search);
const chatId = params.get('chat');
if (chatId) {
const chat = chats.find(c => c.id === chatId);
if (chat) openChat(chat);
}
}, [chats]);
// Handle SW messages
useEffect(() => {
if (!('serviceWorker' in navigator)) return;
const handler = (event: MessageEvent) => {
if (event.data?.type === 'open_chat') {
const chat = chats.find(c => c.id === event.data.chatId);
if (chat) openChat(chat);
}
};
navigator.serviceWorker.addEventListener('message', handler);
return () => navigator.serviceWorker.removeEventListener('message', handler);
}, [chats]);
async function openChat(chat: Chat) {
// Load full chat info
try {
const { data } = await api.get(`/api/chats/${chat.id}`);
const merged = { ...chat, ...data };
setActiveChat(merged);
setFullChat(merged);
updateChat({ id: chat.id, unreadCount: 0 });
wsClient.send('read_messages', { chatId: chat.id });
} catch {
setActiveChat(chat);
setFullChat(chat);
}
setMobileChatOpen(true);
setShowInfo(false);
}
function handleLogout() {
wsClient.disconnect();
logout();
navigate('/login');
}
const filtered = chats.filter(c =>
c.title?.toLowerCase().includes(search.toLowerCase())
);
const canSend = (() => {
if (!activeChat) return false;
if (activeChat.type === 'private') return true;
if (activeChat.type === 'group') return activeChat.canSendMessages !== false;
if (activeChat.type === 'channel') return activeChat.myRole === 'owner' || activeChat.myRole === 'admin' || !!user?.isAdmin;
return false;
})();
return (
<div className="flex h-full bg-gray-100">
{/* Sidebar */}
<div className={`w-full md:w-80 lg:w-96 bg-white flex flex-col border-r border-gray-100 ${mobileChatOpen ? 'hidden md:flex' : 'flex'}`}>
{/* Sidebar Header */}
<div className="px-4 py-3 border-b border-gray-100">
<div className="flex items-center gap-3 mb-3">
<Avatar name={user?.displayName || ''} color={user?.avatarColor || '#3b82f6'} size="sm" />
<div className="flex-1 min-w-0">
<div className="font-semibold text-gray-900 text-sm truncate">{user?.displayName}</div>
<div className="flex items-center gap-1 text-xs text-gray-400">
{connected
? <><Wifi className="w-3 h-3 text-green-400" /> <span>Online</span></>
: <><WifiOff className="w-3 h-3 text-red-400" /> <span>Offline</span></>
}
</div>
</div>
<div className="flex items-center gap-1">
{user?.isAdmin && (
<button onClick={() => setShowAdmin(true)}
className="p-1.5 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600" title="Панель администратора">
<Shield className="w-4 h-4" />
</button>
)}
<button onClick={() => setShowNew(true)}
className="p-1.5 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600" title="Новый чат">
<Edit className="w-4 h-4" />
</button>
<button onClick={handleLogout}
className="p-1.5 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-red-500" title="Выйти">
<LogOut className="w-4 h-4" />
</button>
</div>
</div>
{/* Search */}
<div className="flex items-center gap-2 bg-gray-100 rounded-xl px-3 py-2">
<Search className="w-4 h-4 text-gray-400 flex-shrink-0" />
<input
value={search} onChange={e => setSearch(e.target.value)}
placeholder="Поиск чатов..."
className="flex-1 bg-transparent text-sm outline-none text-gray-700 placeholder-gray-400"
/>
</div>
</div>
{/* Chat List */}
<div className="flex-1 overflow-y-auto py-2 px-2 space-y-0.5">
{filtered.length === 0 && (
<div className="text-center text-gray-400 text-sm py-8">
{search ? 'Ничего не найдено' : 'Нет чатов. Создайте новый!'}
</div>
)}
{filtered.map(chat => (
<ChatListItem
key={chat.id}
chat={chat}
active={activeChat?.id === chat.id}
online={chat.type === 'private' && chat.privateUserId ? onlineUsers.has(chat.privateUserId) : undefined}
onClick={() => openChat(chat)}
/>
))}
</div>
</div>
{/* Main Chat Area */}
<div className={`flex-1 flex flex-col ${!mobileChatOpen ? 'hidden md:flex' : 'flex'}`}>
{activeChat && fullChat ? (
<>
<div className="flex flex-1 overflow-hidden">
<div className="flex-1 flex flex-col min-w-0">
<ChatHeader
chat={fullChat}
onBack={() => { setMobileChatOpen(false); setActiveChat(null); }}
onRefresh={() => openChat(activeChat)}
onShowInfo={() => setShowInfo(!showInfo)}
/>
<div className="flex-1 overflow-hidden bg-gray-50">
<MessageList
chatId={activeChat.id}
isGroup={activeChat.type !== 'private'}
canSend={canSend}
/>
</div>
</div>
{/* Info Panel */}
{showInfo && (
<ChatInfoPanel
chat={fullChat}
onClose={() => setShowInfo(false)}
onRefresh={() => openChat(activeChat)}
/>
)}
</div>
</>
) : (
<div className="hidden md:flex flex-1 items-center justify-center text-gray-400 flex-col gap-4">
<div className="w-24 h-24 bg-blue-100 rounded-full flex items-center justify-center">
<svg className="w-12 h-12 text-blue-400" fill="currentColor" viewBox="0 0 24 24">
<path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/>
</svg>
</div>
<div className="text-center">
<p className="text-lg font-medium text-gray-600">JaniChat</p>
<p className="text-sm text-gray-400 mt-1">Выберите чат или создайте новый</p>
</div>
</div>
)}
</div>
{/* Modals */}
{showNew && <NewChatModal onClose={() => setShowNew(false)} onOpen={(id) => {
const chat = chats.find(c => c.id === id);
if (chat) openChat(chat);
}} />}
{showAdmin && <AdminPanel onClose={() => setShowAdmin(false)} />}
</div>
);
}