v0.11.19 bug fixes

This commit is contained in:
2026-03-22 14:03:22 -04:00
parent bddfcaac7e
commit 2856505f8d
8 changed files with 308 additions and 19 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "jama-backend",
"version": "0.11.18",
"version": "0.11.19",
"description": "TeamChat backend server",
"main": "src/index.js",
"scripts": {

View File

@@ -256,18 +256,34 @@ router.patch('/:id', authMiddleware, teamManagerMiddleware, async (req, res) =>
for (const uid of newIds) {
if (!currentSet.has(uid)) {
await exec(req.schema, 'INSERT INTO user_group_members (user_group_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [ug.id, uid]);
await addUser(req.schema, ug.dm_group_id, uid, req.user.id);
await addUserSilent(req.schema, ug.dm_group_id, uid);
addedUids.push(uid);
}
}
for (const uid of currentSet) {
if (!newIds.has(uid)) {
await exec(req.schema, 'DELETE FROM user_group_members WHERE user_group_id=$1 AND user_id=$2', [ug.id, uid]);
await removeUser(req.schema, ug.dm_group_id, uid, req.user.id);
await exec(req.schema, 'DELETE FROM group_members WHERE group_id=$1 AND user_id=$2', [ug.dm_group_id, uid]);
io.in(R(schema,'user',uid)).socketsLeave(R(schema,'group',ug.dm_group_id));
io.to(R(schema,'user',uid)).emit('group:deleted', { groupId: ug.dm_group_id });
removedUids.push(uid);
}
}
// Notification rule: single user → named message; multiple users → one generic message
if (addedUids.length === 1) {
const u = await queryOne(req.schema, 'SELECT name,display_name FROM users WHERE id=$1', [addedUids[0]]);
await postSysMsg(req.schema, ug.dm_group_id, req.user.id, `${u?.display_name||u?.name||'A user'} has joined the conversation.`);
} else if (addedUids.length > 1) {
await postSysMsg(req.schema, ug.dm_group_id, req.user.id, `${addedUids.length} new members have joined the conversation.`);
}
if (removedUids.length === 1) {
const u = await queryOne(req.schema, 'SELECT name,display_name FROM users WHERE id=$1', [removedUids[0]]);
await postSysMsg(req.schema, ug.dm_group_id, req.user.id, `${u?.display_name||u?.name||'A user'} has been removed from the conversation.`);
} else if (removedUids.length > 1) {
await postSysMsg(req.schema, ug.dm_group_id, req.user.id, `${removedUids.length} members have been removed from the conversation.`);
}
// Propagate to multi-group DMs
const mgDms = await query(req.schema, `
SELECT mgd.id, mgd.dm_group_id FROM multi_group_dm_members mgdm
@@ -287,8 +303,18 @@ router.patch('/:id', authMiddleware, teamManagerMiddleware, async (req, res) =>
io.to(R(schema,'user',uid)).emit('group:deleted', { groupId: mg.dm_group_id });
}
}
if (addedUids.length > 0) await postSysMsg(req.schema, mg.dm_group_id, req.user.id, `Members were added to group "${ug.name}" and have joined this conversation.`);
if (removedUids.length > 0) await postSysMsg(req.schema, mg.dm_group_id, req.user.id, `Members were removed from group "${ug.name}" and have left this conversation.`);
if (addedUids.length === 1) {
const u = await queryOne(req.schema, 'SELECT name,display_name FROM users WHERE id=$1', [addedUids[0]]);
await postSysMsg(req.schema, mg.dm_group_id, req.user.id, `${u?.display_name||u?.name||'A user'} has joined this conversation.`);
} else if (addedUids.length > 1) {
await postSysMsg(req.schema, mg.dm_group_id, req.user.id, `${addedUids.length} new members have joined this conversation.`);
}
if (removedUids.length === 1) {
const u = await queryOne(req.schema, 'SELECT name,display_name FROM users WHERE id=$1', [removedUids[0]]);
await postSysMsg(req.schema, mg.dm_group_id, req.user.id, `${u?.display_name||u?.name||'A user'} has been removed from this conversation.`);
} else if (removedUids.length > 1) {
await postSysMsg(req.schema, mg.dm_group_id, req.user.id, `${removedUids.length} members have been removed from this conversation.`);
}
}
}

View File

@@ -13,7 +13,7 @@
# ─────────────────────────────────────────────────────────────
set -euo pipefail
VERSION="${1:-0.11.18}"
VERSION="${1:-0.11.19}"
ACTION="${2:-}"
REGISTRY="${REGISTRY:-}"
IMAGE_NAME="jama"

View File

@@ -1,6 +1,6 @@
{
"name": "jama-frontend",
"version": "0.11.18",
"version": "0.11.19",
"private": true,
"scripts": {
"dev": "vite",

View File

@@ -253,7 +253,10 @@ export default function ChatWindow({ group, onBack, onGroupUpdated, onDirectMess
{group.is_readonly ? <span className="readonly-badge" style={{ marginLeft: 8 }}>read-only</span> : null}
</div>
{isDirect && <div className="chat-header-sub">Private message</div>}
{!isDirect && group.type === 'private' && <div className="chat-header-sub">Private group</div>}
{!isDirect && group.type === 'public' && <div className="chat-header-sub">Public message</div>}
{!isDirect && group.type === 'private' && group.is_managed && !group.is_multi_group && <div className="chat-header-sub">Private user group</div>}
{!isDirect && group.type === 'private' && group.is_managed && group.is_multi_group && <div className="chat-header-sub">Private group</div>}
{!isDirect && group.type === 'private' && !group.is_managed && <div className="chat-header-sub">Private group</div>}
</div>
<button

View File

@@ -3,21 +3,23 @@ import { useAuth } from '../contexts/AuthContext.jsx';
import './NavDrawer.css';
const NAV_ICON = {
messages: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>,
schedules: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>,
users: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>,
groups: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"/><line x1="12" y1="12" x2="12" y2="16"/><line x1="10" y1="14" x2="14" y2="14"/></svg>,
branding: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="3"/><path d="M12 2a10 10 0 1 0 10 10"/></svg>,
hostpanel: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>,
settings: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93l-1.41 1.41M5.34 18.66l-1.41 1.41M12 2v2M12 20v2M4.93 4.93l1.41 1.41M18.66 18.66l1.41 1.41M2 12h2M20 12h2"/></svg>,
messages: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>,
groupmessages: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>,
schedules: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>,
users: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>,
groups: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"/><line x1="12" y1="12" x2="12" y2="16"/><line x1="10" y1="14" x2="14" y2="14"/></svg>,
branding: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="3"/><path d="M12 2a10 10 0 1 0 10 10"/></svg>,
hostpanel: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>,
settings: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93l-1.41 1.41M5.34 18.66l-1.41 1.41M12 2v2M12 20v2M4.93 4.93l1.41 1.41M18.66 18.66l1.41 1.41M2 12h2M20 12h2"/></svg>,
};
export default function NavDrawer({ open, onClose, onMessages, onSchedule, onScheduleManager, onBranding, onSettings, onUsers, onGroupManager, onHostPanel, features = {}, currentPage = 'chat', isMobile = false }) {
export default function NavDrawer({ open, onClose, onMessages, onGroupMessages, onSchedule, onScheduleManager, onBranding, onSettings, onUsers, onGroupManager, onHostPanel, features = {}, currentPage = 'chat', isMobile = false }) {
const { user } = useAuth();
const drawerRef = useRef(null);
const isAdmin = user?.role === 'admin';
const userGroupIds = features.userGroupMemberships || [];
const canAccessTools = isAdmin || (features.teamToolManagers || []).some(gid => userGroupIds.includes(gid));
const hasUserGroups = userGroupIds.length > 0;
useEffect(() => {
if (!open) return;
@@ -62,7 +64,8 @@ export default function NavDrawer({ open, onClose, onMessages, onSchedule, onSch
</div>
{/* User section */}
{item(NAV_ICON.messages, 'Messages', onMessages, { active: currentPage === 'chat' })}
{item(NAV_ICON.messages, 'Messages', onMessages, { active: currentPage === 'chat' })}
{hasUserGroups && item(NAV_ICON.groupmessages, 'Group Messages', onGroupMessages, { active: currentPage === 'groupmessages' })}
{features.scheduleManager && item(NAV_ICON.schedules, 'Schedules', onSchedule, { active: currentPage === 'schedule' })}
{/* Admin section */}

View File

@@ -7,6 +7,192 @@ import Avatar from './Avatar.jsx';
import './Sidebar.css';
import UserFooter from './UserFooter.jsx';
// Must match Avatar.jsx exactly so sidebar colours are consistent with message avatars
const AVATAR_COLORS = ['#1a73e8','#ea4335','#34a853','#fa7b17','#a142f4','#00897b','#e91e8c','#0097a7'];
function nameToColor(name) {
return AVATAR_COLORS[(name || '').charCodeAt(0) % AVATAR_COLORS.length];
}
function useAppSettings() {
const [settings, setSettings] = useState({ app_name: 'jama', logo_url: '', color_avatar_public: '', color_avatar_dm: '' });
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';
const prefix = document.title.match(/^(\(\d+\)\s*)/)?.[1] || '';
document.title = prefix + name;
const faviconUrl = settings.logo_url || '/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;
}
function formatTime(dateStr) {
if (!dateStr) return '';
const date = parseTS(dateStr);
const now = new Date();
const diff = now - date;
if (diff < 86400000 && date.getDate() === now.getDate()) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
if (diff < 604800000) {
return date.toLocaleDateString([], { weekday: 'short' });
}
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
}
export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifications, unreadGroups = new Map(), onNewChat, onProfile, onUsers, onSettings: onOpenSettings, onBranding, onGroupManager, onGroupsUpdated, isMobile, onAbout, onHelp, onlineUserIds = new Set(), features = {}, groupMessagesMode = false }) {
const { user } = useAuth();
const { connected } = useSocket();
const toast = useToast();
const settings = useAppSettings();
const allGroups = [
...(groups.publicGroups || []),
...(groups.privateGroups || [])
];
const publicFiltered = allGroups.filter(g => g.type === 'public');
// In groupMessagesMode show only managed groups; on main Messages hide managed groups
const privateFiltered = [...allGroups.filter(g => g.type === 'private' && (groupMessagesMode ? g.is_managed : !g.is_managed))].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 getNotifCount = (groupId) => notifications.filter(n => n.groupId === groupId).length;
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 isOnline = !!group.is_direct && !!group.peer_id && (onlineUserIds instanceof Set ? onlineUserIds.has(Number(group.peer_id)) : false);
// Peer avatar colour: use the same algorithm as Avatar.jsx so it matches message bubbles
const peerColor = group.is_direct && !group.is_managed && group.peer_real_name
? nameToColor(group.peer_real_name)
: null;
return (
<div
className={`group-item ${isActive ? 'active' : ''} ${hasUnread ? 'has-unread' : ''}`}
onClick={() => onSelectGroup(group.id)}
>
<div className="group-icon-wrap">
{group.is_direct && group.peer_avatar && !group.is_managed ? (
<img src={group.peer_avatar} alt={group.name} className="group-icon" style={{ objectFit: 'cover', padding: 0 }} />
) : group.is_direct && !group.is_managed ? (
// No custom avatar — use the per-user colour matching Avatar.jsx
<div className="group-icon" style={{ background: peerColor }}>
{(group.peer_real_name || group.name)[0]?.toUpperCase()}
</div>
) : group.is_managed && group.is_multi_group ? (
<div className="group-icon" style={{ background: settings.color_avatar_dm || '#a142f4', borderRadius: 8, fontSize: 11, fontWeight: 700 }}>MG</div>
) : group.is_managed ? (
<div className="group-icon" style={{ background: settings.color_avatar_dm || '#a142f4', borderRadius: 8, fontSize: 11, fontWeight: 700 }}>UG</div>
) : (
<div className="group-icon" style={{ background: group.type === 'public' ? (settings.color_avatar_public || '#1a73e8') : (settings.color_avatar_dm || '#a142f4') }}>
{group.type === 'public' ? '#' : group.name[0]?.toUpperCase()}
</div>
)}
{isOnline && <span className="online-dot" />}
</div>
<div className="group-info flex-1 overflow-hidden">
<div className="flex items-center justify-between">
<span className={`group-name truncate ${hasUnread ? 'unread-name' : ''}`}>
{group.is_direct && group.peer_display_name
? <>{group.peer_display_name}<span className="dm-real-name"> ({group.peer_real_name})</span></>
: group.is_direct && group.peer_real_name ? group.peer_real_name : group.name}
</span>
{group.last_message_at && (
<span className="group-time">{formatTime(group.last_message_at)}</span>
)}
</div>
<div className="flex items-center justify-between gap-2">
<span className="group-last-msg truncate">
{(() => {
const preview = (group.last_message || '').replace(/@\[([^\]]+)\]/g, '@$1');
if (!preview) return group.is_readonly ? '📢 Read-only' : 'No messages yet';
const isOwn = group.last_message_user_id && user && group.last_message_user_id === user.id;
return isOwn ? <><strong style={{ fontWeight: 600 }}>You:</strong> {preview}</> : preview;
})()}
</span>
{notifs > 0 && <span className="badge shrink-0">{notifs}</span>}
{hasUnread && notifs === 0 && <span className="badge badge-unread shrink-0">{unreadCount}</span>}
</div>
</div>
</div>
);
};
return (
<div className="sidebar">
<div className="sidebar-newchat-bar">
{!isMobile && !groupMessagesMode && (
<button className="newchat-btn" onClick={onNewChat}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" width="18" height="18">
<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>
New Chat
</button>
)}
</div>
<div className="groups-list">
{!groupMessagesMode && publicFiltered.length > 0 && (
<div className="group-section">
<div className="section-label">PUBLIC MESSAGES</div>
{publicFiltered.map(g => <GroupItem key={g.id} group={g} />)}
</div>
)}
{!groupMessagesMode && privateFiltered.length > 0 && (
<div className="group-section">
<div className="section-label">PRIVATE MESSAGES</div>
{privateFiltered.map(g => <GroupItem key={g.id} group={g} />)}
</div>
)}
{groupMessagesMode && privateFiltered.length > 0 && (
<div className="group-section">
<div className="section-label">PRIVATE GROUP MESSAGES</div>
{privateFiltered.map(g => <GroupItem key={g.id} group={g} />)}
</div>
)}
{groupMessagesMode && privateFiltered.length === 0 && (
<div style={{ textAlign: 'center', padding: '40px 20px', color: 'var(--text-tertiary)', fontSize: 14 }}>
No group messages yet
</div>
)}
{!groupMessagesMode && allGroups.filter(g => !g.is_managed || g.type === 'public').length === 0 && (
<div style={{ textAlign: 'center', padding: '40px 20px', color: 'var(--text-tertiary)', fontSize: 14 }}>
No chats yet
</div>
)}
</div>
{isMobile && !groupMessagesMode && (
<button className="newchat-fab" onClick={onNewChat} title="New Chat">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" width="24" height="24">
<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>
)}
<UserFooter onProfile={onProfile} onHelp={onHelp} onAbout={onAbout} />
</div>
);
}
function useAppSettings() {
const [settings, setSettings] = useState({ app_name: 'jama', logo_url: '', color_avatar_public: '', color_avatar_dm: '' });
const fetchSettings = () => {

View File

@@ -39,7 +39,7 @@ export default function Chat() {
const [notifications, setNotifications] = useState([]);
const [unreadGroups, setUnreadGroups] = useState(new Map());
const [modal, setModal] = useState(null); // 'profile' | 'users' | 'settings' | 'newchat' | 'help' | 'groupmanager'
const [page, setPage] = useState('chat'); // 'chat' | 'schedule'
const [page, setPage] = useState('chat'); // 'chat' | 'schedule' | 'groupmessages'
const [drawerOpen, setDrawerOpen] = useState(false);
const [features, setFeatures] = useState({ branding: false, groupManager: false, scheduleManager: false, appType: 'JAMA-Chat', teamToolManagers: [], isHostDomain: false });
const [helpDismissed, setHelpDismissed] = useState(true); // true until status loaded
@@ -367,6 +367,7 @@ export default function Chat() {
<NavDrawer
open={drawerOpen} onClose={() => setDrawerOpen(false)}
onMessages={() => { setDrawerOpen(false); setPage('chat'); }}
onGroupMessages={() => { setDrawerOpen(false); setPage('groupmessages'); }}
onSchedule={() => { setDrawerOpen(false); setPage('schedule'); }}
onGroupManager={() => { setDrawerOpen(false); setPage('groups'); }}
onBranding={() => { setDrawerOpen(false); setModal('branding'); }}
@@ -400,6 +401,72 @@ export default function Chat() {
onUsers={() => { setDrawerOpen(false); setPage('users'); }}
onHostPanel={() => { setDrawerOpen(false); setPage('hostpanel'); }}
features={features} currentPage={page} isMobile={isMobile} />
<NavDrawer
open={drawerOpen} onClose={() => setDrawerOpen(false)}
onMessages={() => { setDrawerOpen(false); setPage('chat'); }}
onGroupMessages={() => { setDrawerOpen(false); setPage('groupmessages'); }}
onSchedule={() => { setDrawerOpen(false); setPage('schedule'); }}
onGroupManager={() => { setDrawerOpen(false); setPage('groups'); }}
onBranding={() => { setDrawerOpen(false); setModal('branding'); }}
onSettings={() => { setDrawerOpen(false); setModal('settings'); }}
onUsers={() => { setDrawerOpen(false); setPage('users'); }}
onHostPanel={() => { setDrawerOpen(false); setPage('hostpanel'); }}
features={features} currentPage={page} isMobile={isMobile} />
{modal === 'profile' && <ProfileModal onClose={() => setModal(null)} />}
{modal === 'settings' && <SettingsModal onClose={() => setModal(null)} onFeaturesChanged={setFeatures} />}
{modal === 'branding' && <BrandingModal onClose={() => setModal(null)} />}
{modal === 'help' && <HelpModal onClose={() => setModal(null)} dismissed={helpDismissed} />}
{modal === 'about' && <AboutModal onClose={() => setModal(null)} />}
</div>
);
}
if (page === 'groupmessages') {
return (
<div className="chat-layout">
<GlobalBar isMobile={isMobile} showSidebar={showSidebar} onBurger={() => setDrawerOpen(true)} />
<div className="chat-body">
{(!isMobile || showSidebar) && (
<Sidebar
groups={groups}
activeGroupId={activeGroupId}
onSelectGroup={selectGroup}
notifications={notifications}
unreadGroups={unreadGroups}
onProfile={() => setModal('profile')}
onUsers={() => setPage('users')}
onSettings={() => setModal('settings')}
onBranding={() => setModal('branding')}
onGroupManager={() => setPage('groups')}
features={features}
onGroupsUpdated={loadGroups}
isMobile={isMobile}
onAbout={() => setModal('about')}
onHelp={() => setModal('help')}
onlineUserIds={onlineUserIds}
groupMessagesMode={true} />
)}
{(!isMobile || !showSidebar) && (
<ChatWindow
group={activeGroup}
onBack={isMobile ? () => { setShowSidebar(true); setActiveGroupId(null); } : null}
onGroupUpdated={loadGroups}
onDirectMessage={(g) => { loadGroups(); selectGroup(g.id); }}
onMessageDeleted={handleMessageDeleted}
onlineUserIds={onlineUserIds} />
)}
</div>
<NavDrawer
open={drawerOpen} onClose={() => setDrawerOpen(false)}
onMessages={() => { setDrawerOpen(false); setPage('chat'); }}
onGroupMessages={() => { setDrawerOpen(false); setPage('groupmessages'); }}
onSchedule={() => { setDrawerOpen(false); setPage('schedule'); }}
onGroupManager={() => { setDrawerOpen(false); setPage('groups'); }}
onBranding={() => { setDrawerOpen(false); setModal('branding'); }}
onSettings={() => { setDrawerOpen(false); setModal('settings'); }}
onUsers={() => { setDrawerOpen(false); setPage('users'); }}
onHostPanel={() => { setDrawerOpen(false); setPage('hostpanel'); }}
features={features} currentPage={page} isMobile={isMobile} />
{modal === 'profile' && <ProfileModal onClose={() => setModal(null)} />}
{modal === 'settings' && <SettingsModal onClose={() => setModal(null)} onFeaturesChanged={setFeatures} />}
{modal === 'branding' && <BrandingModal onClose={() => setModal(null)} />}
@@ -420,6 +487,7 @@ export default function Chat() {
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
onMessages={() => { setDrawerOpen(false); setPage('chat'); }}
onGroupMessages={() => { setDrawerOpen(false); setPage('groupmessages'); }}
onSchedule={() => { setDrawerOpen(false); setPage('schedule'); }}
onScheduleManager={() => { setDrawerOpen(false); setPage('schedule'); }}
onGroupManager={() => { setDrawerOpen(false); setPage('groups'); }}
@@ -456,6 +524,7 @@ export default function Chat() {
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
onMessages={() => { setDrawerOpen(false); setPage('chat'); }}
onGroupMessages={() => { setDrawerOpen(false); setPage('groupmessages'); }}
onSchedule={() => { setDrawerOpen(false); setPage('schedule'); }}
onScheduleManager={() => { setDrawerOpen(false); setPage('schedule'); }}
onGroupManager={() => { setDrawerOpen(false); setPage('groups'); }}
@@ -505,7 +574,8 @@ export default function Chat() {
isMobile={isMobile}
onAbout={() => setModal('about')}
onHelp={() => setModal('help')}
onlineUserIds={onlineUserIds} />
onlineUserIds={onlineUserIds}
groupMessagesMode={false} />
)}
{(!isMobile || !showSidebar) && (
@@ -523,6 +593,7 @@ export default function Chat() {
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
onMessages={() => { setDrawerOpen(false); setPage('chat'); }}
onGroupMessages={() => { setDrawerOpen(false); setPage('groupmessages'); }}
onSchedule={() => { setDrawerOpen(false); setPage('schedule'); }}
onScheduleManager={() => { setDrawerOpen(false); setPage('schedule'); }}
onGroupManager={() => { setDrawerOpen(false); setPage('groups'); }}