version 0.0.24
This commit is contained in:
@@ -6,8 +6,19 @@ 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: 'TeamChat', logo_url: '' });
|
||||
const [settings, setSettings] = useState({ app_name: 'jama', logo_url: '' });
|
||||
|
||||
const fetchSettings = () => {
|
||||
api.getSettings().then(({ settings }) => setSettings(settings)).catch(() => {});
|
||||
@@ -16,20 +27,20 @@ function useAppSettings() {
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
// Re-fetch when settings are saved from the SettingsModal
|
||||
window.addEventListener('teamchat:settings-changed', fetchSettings);
|
||||
return () => window.removeEventListener('teamchat:settings-changed', fetchSettings);
|
||||
window.addEventListener('jama:settings-changed', fetchSettings);
|
||||
return () => window.removeEventListener('jama:settings-changed', fetchSettings);
|
||||
}, []);
|
||||
|
||||
// Update page title and favicon whenever settings change
|
||||
useEffect(() => {
|
||||
const name = settings.app_name || 'TeamChat';
|
||||
const name = settings.app_name || 'jama';
|
||||
|
||||
// Update <title>
|
||||
document.title = name;
|
||||
|
||||
// Update favicon
|
||||
const logoUrl = settings.logo_url;
|
||||
const faviconUrl = logoUrl || '/logo.svg';
|
||||
const faviconUrl = logoUrl || '/icons/jama.png';
|
||||
let link = document.querySelector("link[rel~='icon']");
|
||||
if (!link) {
|
||||
link = document.createElement('link');
|
||||
@@ -42,15 +53,16 @@ function useAppSettings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifications, unreadGroups = new Set(), onNewChat, onProfile, onUsers, onSettings: onOpenSettings, onGroupsUpdated }) {
|
||||
export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifications, unreadGroups = new Map(), onNewChat, onProfile, onUsers, onSettings: onOpenSettings, onGroupsUpdated }) {
|
||||
const { user, logout } = useAuth();
|
||||
const { connected } = useSocket();
|
||||
const toast = useToast();
|
||||
const [search, setSearch] = useState('');
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const settings = useAppSettings();
|
||||
const [dark, setDark] = useTheme();
|
||||
|
||||
const appName = settings.app_name || 'TeamChat';
|
||||
const appName = settings.app_name || 'jama';
|
||||
const logoUrl = settings.logo_url;
|
||||
|
||||
const allGroups = [
|
||||
@@ -73,7 +85,8 @@ export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifica
|
||||
|
||||
const GroupItem = ({ group }) => {
|
||||
const notifs = getNotifCount(group.id);
|
||||
const hasUnread = unreadGroups.has(group.id);
|
||||
const unreadCount = unreadGroups.get(group.id) || 0;
|
||||
const hasUnread = unreadCount > 0;
|
||||
const isActive = group.id === activeGroupId;
|
||||
|
||||
return (
|
||||
@@ -93,7 +106,7 @@ export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifica
|
||||
{group.last_message || (group.is_readonly ? '📢 Read-only' : 'No messages yet')}
|
||||
</span>
|
||||
{notifs > 0 && <span className="badge shrink-0">{notifs}</span>}
|
||||
{hasUnread && notifs === 0 && <span className="unread-dot shrink-0" />}
|
||||
{hasUnread && notifs === 0 && <span className="badge badge-unread shrink-0">{unreadCount}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -108,14 +121,7 @@ export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifica
|
||||
{logoUrl ? (
|
||||
<img src={logoUrl} alt={appName} className="sidebar-logo" />
|
||||
) : (
|
||||
<div className="sidebar-logo-default">
|
||||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="24" cy="24" r="24" fill="#1a73e8"/>
|
||||
<path d="M12 16h24v2H12zM12 22h18v2H12zM12 28h20v2H12z" fill="white"/>
|
||||
<circle cx="36" cy="32" r="8" fill="#34a853"/>
|
||||
<path d="M33 32l2 2 4-4" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<img src="/icons/jama.png" alt="jama" className="sidebar-logo" />
|
||||
)}
|
||||
<h2 className="sidebar-title truncate">{appName}</h2>
|
||||
{!connected && <span className="offline-dot" title="Offline" />}
|
||||
@@ -124,9 +130,9 @@ export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifica
|
||||
{settings.icon_newchat ? (
|
||||
<img src={settings.icon_newchat} alt="New Chat" style={{ width: 20, height: 20, objectFit: 'contain' }} />
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" width="30" height="30">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 0 1 .865-.501 48.172 48.172 0 0 0 3.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" width="30" height="30">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 0 1 .865-.501 48.172 48.172 0 0 0 3.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
@@ -145,7 +151,7 @@ export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifica
|
||||
<div className="groups-list">
|
||||
{publicFiltered.length > 0 && (
|
||||
<div className="group-section">
|
||||
<div className="section-label">CHANNELS</div>
|
||||
<div className="section-label">PUBLIC MESSAGES</div>
|
||||
{publicFiltered.map(g => <GroupItem key={g.id} group={g} />)}
|
||||
</div>
|
||||
)}
|
||||
@@ -166,16 +172,40 @@ export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifica
|
||||
|
||||
{/* User footer */}
|
||||
<div className="sidebar-footer">
|
||||
<button className="user-footer-btn" onClick={() => setShowMenu(!showMenu)}>
|
||||
<Avatar user={user} size="sm" />
|
||||
<div className="flex-col flex-1 overflow-hidden" style={{ textAlign: 'left' }}>
|
||||
<span className="font-medium text-sm truncate">{user?.display_name || user?.name}</span>
|
||||
<span className="text-xs truncate" style={{ color: 'var(--text-secondary)' }}>{user?.role}</span>
|
||||
</div>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<button className="user-footer-btn" style={{ flex: 1 }} onClick={() => setShowMenu(!showMenu)}>
|
||||
<Avatar user={user} size="sm" />
|
||||
<div className="flex-col flex-1 overflow-hidden" style={{ textAlign: 'left' }}>
|
||||
<span className="font-medium text-sm truncate">{user?.display_name || user?.name}</span>
|
||||
<span className="text-xs truncate" style={{ color: 'var(--text-secondary)' }}>{user?.role}</span>
|
||||
</div>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="btn-icon"
|
||||
onClick={() => setDark(d => !d)}
|
||||
title={dark ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
style={{ flexShrink: 0, padding: 8 }}
|
||||
>
|
||||
{dark ? (
|
||||
/* Sun icon — click to go light */
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="5"/>
|
||||
<line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
|
||||
<line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
||||
</svg>
|
||||
) : (
|
||||
/* Moon icon — click to go dark */
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showMenu && (
|
||||
<div className="footer-menu" onClick={() => setShowMenu(false)}>
|
||||
|
||||
Reference in New Issue
Block a user