diff --git a/backend/package.json b/backend/package.json
index 8c9f05f..ca1c9a7 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "rosterchirp-backend",
- "version": "0.12.20",
+ "version": "0.12.21",
"description": "RosterChirp backend server",
"main": "src/index.js",
"scripts": {
diff --git a/build.sh b/build.sh
index 8526633..94d4611 100644
--- a/build.sh
+++ b/build.sh
@@ -13,7 +13,7 @@
# ─────────────────────────────────────────────────────────────
set -euo pipefail
-VERSION="${1:-0.12.20}"
+VERSION="${1:-0.12.21}"
ACTION="${2:-}"
REGISTRY="${REGISTRY:-}"
IMAGE_NAME="rosterchirp"
diff --git a/frontend/package.json b/frontend/package.json
index cad5604..01c41c0 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "rosterchirp-frontend",
- "version": "0.12.20",
+ "version": "0.12.21",
"private": true,
"scripts": {
"dev": "vite",
diff --git a/frontend/src/pages/GroupManagerPage.jsx b/frontend/src/pages/GroupManagerPage.jsx
index 20c1eb8..b3ee0bd 100644
--- a/frontend/src/pages/GroupManagerPage.jsx
+++ b/frontend/src/pages/GroupManagerPage.jsx
@@ -72,6 +72,7 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) {
const [saving, setSaving] = useState(false);
const [deleting, setDeleting] = useState(false);
const [showDelete, setShowDelete] = useState(false);
+ const [accordionOpen, setAccordionOpen] = useState(false);
const load = useCallback(() =>
api.getUserGroups().then(({ groups }) => setGroups([...(groups||[])].sort((a, b) => a.name.localeCompare(b.name)))).catch(() => {}), []);
@@ -79,6 +80,7 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) {
const selectGroup = async (g) => {
setShowDelete(false);
+ setAccordionOpen(false);
const { members: mems } = await api.getUserGroup(g.id);
const ids = new Set(mems.map(m => m.id));
setSelected(g); setEditName(g.name); setMembers(ids); setSavedMembers(ids);
@@ -141,24 +143,58 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) {
return (
- {/* Sidebar list */}
-
-
User Groups
-
- {groups.map(g => (
-
+ )}
{/* Form */}
@@ -238,6 +274,7 @@ function DirectMessagesTab({ allUserGroups, onRefresh, refreshKey, isMobile = fa
const [saving, setSaving] = useState(false);
const [deleting, setDeleting] = useState(false);
const [showDelete, setShowDelete] = useState(false);
+ const [accordionOpen, setAccordionOpen] = useState(false);
const load = useCallback(() =>
api.getMultiGroupDms().then(({ dms }) => setDms([...(dms||[])].sort((a, b) => a.name.localeCompare(b.name)))).catch(() => {}), []);
@@ -245,7 +282,7 @@ function DirectMessagesTab({ allUserGroups, onRefresh, refreshKey, isMobile = fa
const clearSelection = () => { setSelected(null); setDmName(''); setGroupIds(new Set()); setSavedGroupIds(new Set()); setShowDelete(false); };
const selectDm = (dm) => {
- setShowDelete(false); setSelected(dm); setDmName(dm.name);
+ setShowDelete(false); setAccordionOpen(false); setSelected(dm); setDmName(dm.name);
const ids = new Set(dm.memberGroupIds||[]); setGroupIds(ids); setSavedGroupIds(ids);
};
@@ -282,23 +319,59 @@ function DirectMessagesTab({ allUserGroups, onRefresh, refreshKey, isMobile = fa
return (
-
-
Multi-Group DMs
-
+ New Multi-Group DM
- {dms.map(dm => (
-
selectDm(dm)} style={{ display:'block', width:'100%', textAlign:'left', padding:'8px 10px', borderRadius:'var(--radius)', border:'none',
- background:selected?.id===dm.id?'var(--primary-light)':'transparent', color:selected?.id===dm.id?'var(--primary)':'var(--text-primary)',
- cursor:'pointer', fontWeight:selected?.id===dm.id?600:400, fontSize:13, marginBottom:2 }}>
-
-
MG
-
{dm.name}
{dm.group_count} group{dm.group_count!==1?'s':''}
-
+
+ {/* Sidebar — desktop only */}
+ {!isMobile && (
+
+
Multi-Group DMs
+
+ New Multi-Group DM
+ {dms.map(dm => (
+
selectDm(dm)} style={{ display:'block', width:'100%', textAlign:'left', padding:'8px 10px', borderRadius:'var(--radius)', border:'none',
+ background:selected?.id===dm.id?'var(--primary-light)':'transparent', color:selected?.id===dm.id?'var(--primary)':'var(--text-primary)',
+ cursor:'pointer', fontWeight:selected?.id===dm.id?600:400, fontSize:13, marginBottom:2 }}>
+
+
MG
+
{dm.name}
{dm.group_count} group{dm.group_count!==1?'s':''}
+
+
+ ))}
+ {dms.length===0 &&
No multi-group DMs yet
}
+
+ )}
+
+ {/* Mobile accordion */}
+ {isMobile && (
+
+
+ New Multi-Group DM
+
setAccordionOpen(o => !o)} style={{ display:'flex', alignItems:'center', justifyContent:'space-between', width:'100%', padding:'8px 10px',
+ borderRadius:'var(--radius)', border:'1px solid var(--border)', background:'var(--surface)', cursor:'pointer',
+ fontSize:13, fontWeight:600, color:'var(--text-primary)', marginBottom: accordionOpen ? 4 : 0 }}>
+ Edit Existing
+ {accordionOpen ? '▲' : '▼'}
- ))}
- {dms.length===0 &&
No multi-group DMs yet
}
-
+ {accordionOpen && (
+
+ {dms.map(dm => (
+
selectDm(dm)} style={{ display:'block', width:'100%', textAlign:'left', padding:'8px 12px',
+ border:'none', borderBottom:'1px solid var(--border)',
+ background:selected?.id===dm.id?'var(--primary-light)':'transparent', color:selected?.id===dm.id?'var(--primary)':'var(--text-primary)',
+ cursor:'pointer', fontWeight:selected?.id===dm.id?600:400, fontSize:13 }}>
+
+
MG
+
{dm.name}
{dm.group_count} group{dm.group_count!==1?'s':''}
+
+
+ ))}
+ {dms.length===0 &&
No multi-group DMs yet
}
+
+ )}
+
+ )}
+
@@ -345,6 +418,7 @@ function U2URestrictionsTab({ allUserGroups, isMobile = false, onIF, onIB }) {
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [search, setSearch] = useState('');
+ const [accordionOpen, setAccordionOpen] = useState(true);
// Map of groupId → number of restrictions (for showing dots in sidebar)
const [restrictionCounts, setRestrictionCounts] = useState({});
@@ -376,6 +450,7 @@ function U2URestrictionsTab({ allUserGroups, isMobile = false, onIF, onIB }) {
const selectGroup = (g) => {
setSelectedGroup(g);
setSearch('');
+ setAccordionOpen(false);
loadRestrictions(g);
};
@@ -413,40 +488,80 @@ function U2URestrictionsTab({ allUserGroups, isMobile = false, onIF, onIB }) {
? otherGroups.filter(g => g.name.toLowerCase().includes(search.toLowerCase()))
: otherGroups;
+ const u2uGroupButton = (g) => {
+ const hasRestrictions = g.id === selectedGroup?.id ? blockedIds.size > 0 : (restrictionCounts[g.id] || 0) > 0;
+ return (
+
selectGroup(g)} style={{
+ display:'block', width:'100%', textAlign:'left', padding:'8px 10px',
+ borderRadius:'var(--radius)', border:'none',
+ background: selectedGroup?.id===g.id ? 'var(--primary-light)' : 'transparent',
+ color: selectedGroup?.id===g.id ? 'var(--primary)' : 'var(--text-primary)',
+ cursor:'pointer', fontWeight: selectedGroup?.id===g.id ? 600 : 400, fontSize:13, marginBottom:2,
+ }}>
+
+
UG
+
+
{g.name}
+
{g.member_count} member{g.member_count!==1?'s':''}
+
+ {hasRestrictions && (
+
+ )}
+
+
+ );
+ };
+
return (
- {/* Group selector sidebar */}
-
-
- Select Group
+
+ {/* Group selector — desktop sidebar */}
+ {!isMobile && (
+
+
+ Select Group
+
+ {allUserGroups.map(g => u2uGroupButton(g))}
+ {allUserGroups.length === 0 && (
+
No user groups yet
+ )}
- {allUserGroups.map(g => {
- const hasRestrictions = g.id === selectedGroup?.id ? blockedIds.size > 0 : (restrictionCounts[g.id] || 0) > 0;
- return (
-
selectGroup(g)} style={{
- display:'block', width:'100%', textAlign:'left', padding:'8px 10px',
- borderRadius:'var(--radius)', border:'none',
- background: selectedGroup?.id===g.id ? 'var(--primary-light)' : 'transparent',
- color: selectedGroup?.id===g.id ? 'var(--primary)' : 'var(--text-primary)',
- cursor:'pointer', fontWeight: selectedGroup?.id===g.id ? 600 : 400, fontSize:13, marginBottom:2,
- }}>
-
-
UG
-
-
{g.name}
-
{g.member_count} member{g.member_count!==1?'s':''}
-
- {hasRestrictions && (
-
- )}
-
-
- );
- })}
- {allUserGroups.length === 0 && (
-
No user groups yet
- )}
-
+ )}
+
+ {/* Mobile accordion — expanded by default, collapses on selection */}
+ {isMobile && (
+
+
setAccordionOpen(o => !o)} style={{ display:'flex', alignItems:'center', justifyContent:'space-between', width:'100%', padding:'8px 10px',
+ borderRadius:'var(--radius)', border:'1px solid var(--border)', background:'var(--surface)', cursor:'pointer',
+ fontSize:13, fontWeight:600, color:'var(--text-primary)', marginBottom: accordionOpen ? 4 : 0 }}>
+ Select Group
+ {accordionOpen ? '▲' : '▼'}
+
+ {accordionOpen && (
+
+ {allUserGroups.map(g => {
+ const hasRestrictions = g.id === selectedGroup?.id ? blockedIds.size > 0 : (restrictionCounts[g.id] || 0) > 0;
+ return (
+
selectGroup(g)} style={{ display:'block', width:'100%', textAlign:'left', padding:'8px 12px',
+ border:'none', borderBottom:'1px solid var(--border)',
+ background:selectedGroup?.id===g.id?'var(--primary-light)':'transparent', color:selectedGroup?.id===g.id?'var(--primary)':'var(--text-primary)',
+ cursor:'pointer', fontWeight:selectedGroup?.id===g.id?600:400, fontSize:13 }}>
+
+
UG
+
+
{g.name}
+
{g.member_count} member{g.member_count!==1?'s':''}
+
+ {hasRestrictions &&
}
+
+
+ );
+ })}
+ {allUserGroups.length === 0 &&
No user groups yet
}
+
+ )}
+
+ )}
{/* Restriction editor */}