import { useState, useEffect, useRef } from 'react'; import { useAuth } from '../contexts/AuthContext.jsx'; import { useSocket } from '../contexts/SocketContext.jsx'; import { api, parseTS } from '../utils/api.js'; import { useToast } from '../contexts/ToastContext.jsx'; import Avatar from './Avatar.jsx'; import './Sidebar.css'; function useTheme() { const [dark, setDark] = useState(() => localStorage.getItem('jama-theme') === 'dark'); useEffect(() => { document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light'); localStorage.setItem('jama-theme', dark ? 'dark' : 'light'); }, [dark]); return [dark, setDark]; } function useAppSettings() { const [settings, setSettings] = useState({ app_name: 'jama', logo_url: '' }); const fetchSettings = () => { api.getSettings().then(({ settings }) => setSettings(settings)).catch(() => {}); }; useEffect(() => { fetchSettings(); window.addEventListener('jama:settings-changed', fetchSettings); return () => window.removeEventListener('jama:settings-changed', fetchSettings); }, []); useEffect(() => { const name = settings.app_name || 'jama'; // Preserve any unread badge prefix already set by Chat.jsx const prefix = document.title.match(/^(\(\d+\)\s*)/)?.[1] || ''; document.title = prefix + name; const logoUrl = settings.logo_url; const faviconUrl = logoUrl || '/icons/jama.png'; let link = document.querySelector("link[rel~='icon']"); if (!link) { link = document.createElement('link'); link.rel = 'icon'; document.head.appendChild(link); } link.href = faviconUrl; }, [settings]); return settings; } export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifications, unreadGroups = new Map(), onNewChat, onProfile, onUsers, onSettings: onOpenSettings, onGroupsUpdated, isMobile, onAbout, onHelp, onlineUserIds = new Set() }) { const { user, logout } = useAuth(); const { connected } = useSocket(); const toast = useToast(); const [showMenu, setShowMenu] = useState(false); const [contextMenu, setContextMenu] = useState(null); // { groupId, x, y, isPinned } const settings = useAppSettings(); const [dark, setDark] = useTheme(); const menuRef = useRef(null); const footerBtnRef = useRef(null); const handlePin = async (groupId) => { try { await api.pinDM(groupId); onGroupsUpdated(); } catch (e) { toast(e.message, 'error'); } setContextMenu(null); }; const handleUnpin = async (groupId) => { try { await api.unpinDM(groupId); onGroupsUpdated(); } catch (e) { toast(e.message, 'error'); } setContextMenu(null); }; // Close context menu on outside click useEffect(() => { if (!contextMenu) return; const close = () => setContextMenu(null); document.addEventListener('mousedown', close); document.addEventListener('touchstart', close); return () => { document.removeEventListener('mousedown', close); document.removeEventListener('touchstart', close); }; }, [contextMenu]); // Fix 6: swipe right to go back on mobile — handled in ChatWindow, but prevent sidebar swipe exit // Close menu on click outside useEffect(() => { if (!showMenu) return; const handler = (e) => { if (menuRef.current && !menuRef.current.contains(e.target) && footerBtnRef.current && !footerBtnRef.current.contains(e.target)) { setShowMenu(false); } }; document.addEventListener('mousedown', handler); document.addEventListener('touchstart', handler); return () => { document.removeEventListener('mousedown', handler); document.removeEventListener('touchstart', handler); }; }, [showMenu]); const appName = settings.app_name || 'jama'; const logoUrl = settings.logo_url; const allGroups = [ ...(groups.publicGroups || []), ...(groups.privateGroups || []) ]; const publicFiltered = allGroups.filter(g => g.type === 'public'); const pinnedDMs = allGroups .filter(g => g.type === 'private' && g.is_direct && g.pin_order != null) .sort((a, b) => a.pin_order - b.pin_order); const unpinnedDMs = allGroups .filter(g => g.type === 'private' && g.is_direct && g.pin_order == null) .sort((a, b) => { if (!a.last_message_at && !b.last_message_at) return 0; if (!a.last_message_at) return 1; if (!b.last_message_at) return -1; return new Date(b.last_message_at) - new Date(a.last_message_at); }); const privateNonDM = allGroups.filter(g => g.type === 'private' && !g.is_direct); const privateFiltered = [...privateNonDM, ...pinnedDMs, ...unpinnedDMs]; const pinnedGroupIds = new Set(pinnedDMs.map(g => g.id)); const getNotifCount = (groupId) => notifications.filter(n => n.groupId === groupId).length; const handleLogout = async () => { await logout(); }; const GroupItem = ({ group }) => { const notifs = getNotifCount(group.id); const unreadCount = unreadGroups.get(group.id) || 0; const hasUnread = unreadCount > 0; const isActive = group.id === activeGroupId; const isPinned = pinnedGroupIds.has(group.id); const isOnline = group.is_direct && group.peer_id && onlineUserIds.has(group.peer_id); const handleContextMenu = (e) => { if (!group.is_direct) return; e.preventDefault(); e.stopPropagation(); setContextMenu({ groupId: group.id, x: e.clientX, y: e.clientY, isPinned }); }; return (