diff --git a/backend/package.json b/backend/package.json index 22c3a5a..142bdd0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "rosterchirp-backend", - "version": "0.12.14", + "version": "0.12.15", "description": "RosterChirp backend server", "main": "src/index.js", "scripts": { diff --git a/backend/src/routes/usergroups.js b/backend/src/routes/usergroups.js index 001a8e0..de49512 100644 --- a/backend/src/routes/usergroups.js +++ b/backend/src/routes/usergroups.js @@ -156,8 +156,8 @@ router.patch('/multigroup/:id', authMiddleware, teamManagerMiddleware, async (re `, [mg.id, uid]); if (!stillIn) { await exec(req.schema, 'DELETE FROM group_members WHERE group_id=$1 AND user_id=$2', [mg.dm_group_id, uid]); - io.in(R(schema,'user',uid)).socketsLeave(R(schema,'group',mg.dm_group_id)); - io.to(R(schema,'user',uid)).emit('group:deleted', { groupId: mg.dm_group_id }); + io.in(R(req.schema,'user',uid)).socketsLeave(R(req.schema,'group',mg.dm_group_id)); + io.to(R(req.schema,'user',uid)).emit('group:deleted', { groupId: mg.dm_group_id }); } } await postSysMsg(req.schema, mg.dm_group_id, req.user.id, `A group has been removed from this conversation.`); @@ -175,7 +175,7 @@ router.delete('/multigroup/:id', authMiddleware, teamManagerMiddleware, async (r if (mg.dm_group_id) { const members = (await query(req.schema, 'SELECT user_id FROM group_members WHERE group_id=$1', [mg.dm_group_id])).map(r => r.user_id); await exec(req.schema, 'DELETE FROM groups WHERE id=$1', [mg.dm_group_id]); - for (const uid of members) io.to(R(schema,'user',uid)).emit('group:deleted', { groupId: mg.dm_group_id }); + for (const uid of members) io.to(R(req.schema,'user',uid)).emit('group:deleted', { groupId: mg.dm_group_id }); } await exec(req.schema, 'DELETE FROM multi_group_dms WHERE id=$1', [mg.id]); res.json({ success: true }); @@ -268,8 +268,8 @@ router.patch('/:id', authMiddleware, teamManagerMiddleware, async (req, res) => 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 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 }); + io.in(R(req.schema,'user',uid)).socketsLeave(R(req.schema,'group',ug.dm_group_id)); + io.to(R(req.schema,'user',uid)).emit('group:deleted', { groupId: ug.dm_group_id }); removedUids.push(uid); } } @@ -303,8 +303,8 @@ router.patch('/:id', authMiddleware, teamManagerMiddleware, async (req, res) => `, [mg.id, uid]); if (!stillIn) { await exec(req.schema, 'DELETE FROM group_members WHERE group_id=$1 AND user_id=$2', [mg.dm_group_id, uid]); - io.in(R(schema,'user',uid)).socketsLeave(R(schema,'group',mg.dm_group_id)); - io.to(R(schema,'user',uid)).emit('group:deleted', { groupId: mg.dm_group_id }); + io.in(R(req.schema,'user',uid)).socketsLeave(R(req.schema,'group',mg.dm_group_id)); + io.to(R(req.schema,'user',uid)).emit('group:deleted', { groupId: mg.dm_group_id }); } } if (addedUids.length === 1) { @@ -335,7 +335,7 @@ router.delete('/:id', authMiddleware, teamManagerMiddleware, async (req, res) => if (ug.dm_group_id) { const members = (await query(req.schema, 'SELECT user_id FROM group_members WHERE group_id=$1', [ug.dm_group_id])).map(r => r.user_id); await exec(req.schema, 'DELETE FROM groups WHERE id=$1', [ug.dm_group_id]); - for (const uid of members) { 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 }); } + for (const uid of members) { io.in(R(req.schema,'user',uid)).socketsLeave(R(req.schema,'group',ug.dm_group_id)); io.to(R(req.schema,'user',uid)).emit('group:deleted', { groupId: ug.dm_group_id }); } } await exec(req.schema, 'DELETE FROM user_groups WHERE id=$1', [ug.id]); res.json({ success: true }); diff --git a/build.sh b/build.sh index 52c5b02..0309ee2 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.12.14}" +VERSION="${1:-0.12.15}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="rosterchirp" diff --git a/frontend/package.json b/frontend/package.json index 5befef1..6bb948a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "rosterchirp-frontend", - "version": "0.12.14", + "version": "0.12.15", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/src/components/ProfileModal.jsx b/frontend/src/components/ProfileModal.jsx index cf76e7e..34cb13f 100644 --- a/frontend/src/components/ProfileModal.jsx +++ b/frontend/src/components/ProfileModal.jsx @@ -19,6 +19,9 @@ export default function ProfileModal({ onClose }) { const [tab, setTab] = useState('profile'); // 'profile' | 'password' | 'notifications' const [pushTesting, setPushTesting] = useState(false); const [pushResult, setPushResult] = useState(null); + const [notifPermission, setNotifPermission] = useState( + typeof Notification !== 'undefined' ? Notification.permission : 'unsupported' + ); const [hideAdminTag, setHideAdminTag] = useState(!!user?.hide_admin_tag); const [allowDm, setAllowDm] = useState(user?.allow_dm !== 0); @@ -172,58 +175,77 @@ export default function ProfileModal({ onClose }) { {tab === 'notifications' && (
-
-

Tap Send Test Notification to trigger a push to this device. The notification will arrive shortly if everything is configured correctly.

-

If it doesn't arrive, check:
- • Notification permission granted (browser prompt)
- • Android Settings → Apps → RosterChirp → Notifications → Enabled
- • App is backgrounded when the test fires -

-
-
- - -
-
- Test (via SW) — normal production path, service worker shows notification.
- Test (via Browser) — bypasses service worker; Chrome displays directly. -
+ {notifPermission !== 'granted' && notifPermission !== 'unsupported' && ( +
+
+ {notifPermission === 'denied' + ? 'Notifications are blocked. Enable them in Android Settings → Apps → RosterChirp → Notifications.' + : 'Push notifications are not yet enabled on this device.'} +
+ {notifPermission === 'default' && ( + + )} +
+ )} + {notifPermission === 'granted' && ( +
+

Tap Send Test Notification to trigger a push to this device. The notification will arrive shortly if everything is configured correctly.

+

If it doesn't arrive, check:
+ • Android Settings → Apps → RosterChirp → Notifications → Enabled
+ • App is backgrounded when the test fires +

+
+ )} + {notifPermission === 'granted' && (<> +
+ + +
+
+ Test (via SW) — normal production path, service worker shows notification.
+ Test (via Browser) — bypasses service worker; Chrome displays directly. +
+ )} {pushResult && (
{ if (document.visibilityState === 'visible') registerPush(); }; + const handlePushInit = () => registerPush(); document.addEventListener('visibilitychange', handleVisibility); - return () => document.removeEventListener('visibilitychange', handleVisibility); + window.addEventListener('rosterchirp:push-init', handlePushInit); + return () => { + document.removeEventListener('visibilitychange', handleVisibility); + window.removeEventListener('rosterchirp:push-init', handlePushInit); + }; }, []); // When a message is deleted, update the sidebar preview immediately.