diff --git a/backend/package.json b/backend/package.json index 5ecb7b7..68bbcda 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "jama-backend", - "version": "0.11.18", + "version": "0.11.19", "description": "TeamChat backend server", "main": "src/index.js", "scripts": { diff --git a/backend/src/routes/usergroups.js b/backend/src/routes/usergroups.js index 20d6509..ba4106e 100644 --- a/backend/src/routes/usergroups.js +++ b/backend/src/routes/usergroups.js @@ -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.`); + } } } diff --git a/build.sh b/build.sh index 40119d3..cbb52f6 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.11.18}" +VERSION="${1:-0.11.19}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="jama" diff --git a/frontend/package.json b/frontend/package.json index d9b7db3..ef9253c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "jama-frontend", - "version": "0.11.18", + "version": "0.11.19", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/src/components/ChatWindow.jsx b/frontend/src/components/ChatWindow.jsx index 372d7d1..ea947b1 100644 --- a/frontend/src/components/ChatWindow.jsx +++ b/frontend/src/components/ChatWindow.jsx @@ -253,7 +253,10 @@ export default function ChatWindow({ group, onBack, onGroupUpdated, onDirectMess {group.is_readonly ? read-only : null} {isDirect &&
Private message
} - {!isDirect && group.type === 'private' &&
Private group
} + {!isDirect && group.type === 'public' &&
Public message
} + {!isDirect && group.type === 'private' && group.is_managed && !group.is_multi_group &&
Private user group
} + {!isDirect && group.type === 'private' && group.is_managed && group.is_multi_group &&
Private group
} + {!isDirect && group.type === 'private' && !group.is_managed &&
Private group
} + )} + + +
+ {!groupMessagesMode && publicFiltered.length > 0 && ( +
+
PUBLIC MESSAGES
+ {publicFiltered.map(g => )} +
+ )} + {!groupMessagesMode && privateFiltered.length > 0 && ( +
+
PRIVATE MESSAGES
+ {privateFiltered.map(g => )} +
+ )} + {groupMessagesMode && privateFiltered.length > 0 && ( +
+
PRIVATE GROUP MESSAGES
+ {privateFiltered.map(g => )} +
+ )} + {groupMessagesMode && privateFiltered.length === 0 && ( +
+ No group messages yet +
+ )} + {!groupMessagesMode && allGroups.filter(g => !g.is_managed || g.type === 'public').length === 0 && ( +
+ No chats yet +
+ )} +
+ + {isMobile && !groupMessagesMode && ( + + )} + + + + ); +} + function useAppSettings() { const [settings, setSettings] = useState({ app_name: 'jama', logo_url: '', color_avatar_public: '', color_avatar_dm: '' }); const fetchSettings = () => { diff --git a/frontend/src/pages/Chat.jsx b/frontend/src/pages/Chat.jsx index 8d89d4a..5fc8352 100644 --- a/frontend/src/pages/Chat.jsx +++ b/frontend/src/pages/Chat.jsx @@ -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() { 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} /> + 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' && setModal(null)} />} + {modal === 'settings' && setModal(null)} onFeaturesChanged={setFeatures} />} + {modal === 'branding' && setModal(null)} />} + {modal === 'help' && setModal(null)} dismissed={helpDismissed} />} + {modal === 'about' && setModal(null)} />} + + ); + } + + if (page === 'groupmessages') { + return ( +
+ setDrawerOpen(true)} /> +
+ {(!isMobile || showSidebar) && ( + 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) && ( + { setShowSidebar(true); setActiveGroupId(null); } : null} + onGroupUpdated={loadGroups} + onDirectMessage={(g) => { loadGroups(); selectGroup(g.id); }} + onMessageDeleted={handleMessageDeleted} + onlineUserIds={onlineUserIds} /> + )} +
+ 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' && setModal(null)} />} {modal === 'settings' && setModal(null)} onFeaturesChanged={setFeatures} />} {modal === 'branding' && 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'); }}