v0.12.15 PWA bug and schema error for GM
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "rosterchirp-backend",
|
"name": "rosterchirp-backend",
|
||||||
"version": "0.12.14",
|
"version": "0.12.15",
|
||||||
"description": "RosterChirp backend server",
|
"description": "RosterChirp backend server",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -156,8 +156,8 @@ router.patch('/multigroup/:id', authMiddleware, teamManagerMiddleware, async (re
|
|||||||
`, [mg.id, uid]);
|
`, [mg.id, uid]);
|
||||||
if (!stillIn) {
|
if (!stillIn) {
|
||||||
await exec(req.schema, 'DELETE FROM group_members WHERE group_id=$1 AND user_id=$2', [mg.dm_group_id, uid]);
|
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.in(R(req.schema,'user',uid)).socketsLeave(R(req.schema,'group',mg.dm_group_id));
|
||||||
io.to(R(schema,'user',uid)).emit('group:deleted', { groupId: 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.`);
|
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) {
|
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);
|
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]);
|
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]);
|
await exec(req.schema, 'DELETE FROM multi_group_dms WHERE id=$1', [mg.id]);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
@@ -268,8 +268,8 @@ router.patch('/:id', authMiddleware, teamManagerMiddleware, async (req, res) =>
|
|||||||
if (!newIds.has(uid)) {
|
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 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]);
|
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.in(R(req.schema,'user',uid)).socketsLeave(R(req.schema,'group',ug.dm_group_id));
|
||||||
io.to(R(schema,'user',uid)).emit('group:deleted', { groupId: ug.dm_group_id });
|
io.to(R(req.schema,'user',uid)).emit('group:deleted', { groupId: ug.dm_group_id });
|
||||||
removedUids.push(uid);
|
removedUids.push(uid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -303,8 +303,8 @@ router.patch('/:id', authMiddleware, teamManagerMiddleware, async (req, res) =>
|
|||||||
`, [mg.id, uid]);
|
`, [mg.id, uid]);
|
||||||
if (!stillIn) {
|
if (!stillIn) {
|
||||||
await exec(req.schema, 'DELETE FROM group_members WHERE group_id=$1 AND user_id=$2', [mg.dm_group_id, uid]);
|
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.in(R(req.schema,'user',uid)).socketsLeave(R(req.schema,'group',mg.dm_group_id));
|
||||||
io.to(R(schema,'user',uid)).emit('group:deleted', { groupId: mg.dm_group_id });
|
io.to(R(req.schema,'user',uid)).emit('group:deleted', { groupId: mg.dm_group_id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (addedUids.length === 1) {
|
if (addedUids.length === 1) {
|
||||||
@@ -335,7 +335,7 @@ router.delete('/:id', authMiddleware, teamManagerMiddleware, async (req, res) =>
|
|||||||
if (ug.dm_group_id) {
|
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);
|
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]);
|
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]);
|
await exec(req.schema, 'DELETE FROM user_groups WHERE id=$1', [ug.id]);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
|
|||||||
2
build.sh
2
build.sh
@@ -13,7 +13,7 @@
|
|||||||
# ─────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="${1:-0.12.14}"
|
VERSION="${1:-0.12.15}"
|
||||||
ACTION="${2:-}"
|
ACTION="${2:-}"
|
||||||
REGISTRY="${REGISTRY:-}"
|
REGISTRY="${REGISTRY:-}"
|
||||||
IMAGE_NAME="rosterchirp"
|
IMAGE_NAME="rosterchirp"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "rosterchirp-frontend",
|
"name": "rosterchirp-frontend",
|
||||||
"version": "0.12.14",
|
"version": "0.12.15",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ export default function ProfileModal({ onClose }) {
|
|||||||
const [tab, setTab] = useState('profile'); // 'profile' | 'password' | 'notifications'
|
const [tab, setTab] = useState('profile'); // 'profile' | 'password' | 'notifications'
|
||||||
const [pushTesting, setPushTesting] = useState(false);
|
const [pushTesting, setPushTesting] = useState(false);
|
||||||
const [pushResult, setPushResult] = useState(null);
|
const [pushResult, setPushResult] = useState(null);
|
||||||
|
const [notifPermission, setNotifPermission] = useState(
|
||||||
|
typeof Notification !== 'undefined' ? Notification.permission : 'unsupported'
|
||||||
|
);
|
||||||
const [hideAdminTag, setHideAdminTag] = useState(!!user?.hide_admin_tag);
|
const [hideAdminTag, setHideAdminTag] = useState(!!user?.hide_admin_tag);
|
||||||
const [allowDm, setAllowDm] = useState(user?.allow_dm !== 0);
|
const [allowDm, setAllowDm] = useState(user?.allow_dm !== 0);
|
||||||
|
|
||||||
@@ -172,58 +175,77 @@ export default function ProfileModal({ onClose }) {
|
|||||||
|
|
||||||
{tab === 'notifications' && (
|
{tab === 'notifications' && (
|
||||||
<div className="flex-col gap-3">
|
<div className="flex-col gap-3">
|
||||||
<div style={{ fontSize: 14, color: 'var(--text-secondary)', lineHeight: 1.5 }}>
|
{notifPermission !== 'granted' && notifPermission !== 'unsupported' && (
|
||||||
<p style={{ margin: '0 0 8px' }}>Tap <strong>Send Test Notification</strong> to trigger a push to this device. The notification will arrive shortly if everything is configured correctly.</p>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, padding: '10px 12px', borderRadius: 8, background: 'var(--surface-variant)' }}>
|
||||||
<p style={{ margin: 0 }}>If it doesn't arrive, check:<br/>
|
<div style={{ fontSize: 14, color: 'var(--text-secondary)' }}>
|
||||||
• Notification permission granted (browser prompt)<br/>
|
{notifPermission === 'denied'
|
||||||
• Android Settings → Apps → RosterChirp → Notifications → Enabled<br/>
|
? 'Notifications are blocked. Enable them in Android Settings → Apps → RosterChirp → Notifications.'
|
||||||
• App is backgrounded when the test fires
|
: 'Push notifications are not yet enabled on this device.'}
|
||||||
</p>
|
</div>
|
||||||
</div>
|
{notifPermission === 'default' && (
|
||||||
<div className="flex gap-2">
|
<button className="btn btn-primary btn-sm" onClick={async () => {
|
||||||
<button
|
const result = await Notification.requestPermission();
|
||||||
className="btn btn-primary"
|
setNotifPermission(result);
|
||||||
style={{ flex: 1 }}
|
if (result === 'granted') window.dispatchEvent(new CustomEvent('rosterchirp:push-init'));
|
||||||
disabled={pushTesting}
|
}}>Enable Notifications</button>
|
||||||
onClick={async () => {
|
)}
|
||||||
setPushTesting(true);
|
</div>
|
||||||
setPushResult(null);
|
)}
|
||||||
try {
|
{notifPermission === 'granted' && (
|
||||||
const { results } = await api.testPush('data');
|
<div style={{ fontSize: 14, color: 'var(--text-secondary)', lineHeight: 1.5 }}>
|
||||||
setPushResult({ ok: true, results, mode: 'data' });
|
<p style={{ margin: '0 0 8px' }}>Tap <strong>Send Test Notification</strong> to trigger a push to this device. The notification will arrive shortly if everything is configured correctly.</p>
|
||||||
} catch (e) {
|
<p style={{ margin: 0 }}>If it doesn't arrive, check:<br/>
|
||||||
setPushResult({ ok: false, error: e.message });
|
• Android Settings → Apps → RosterChirp → Notifications → Enabled<br/>
|
||||||
} finally {
|
• App is backgrounded when the test fires
|
||||||
setPushTesting(false);
|
</p>
|
||||||
}
|
</div>
|
||||||
}}
|
)}
|
||||||
>
|
{notifPermission === 'granted' && (<>
|
||||||
{pushTesting ? 'Sending…' : 'Test (via SW)'}
|
<div className="flex gap-2">
|
||||||
</button>
|
<button
|
||||||
<button
|
className="btn btn-primary"
|
||||||
className="btn btn-secondary"
|
style={{ flex: 1 }}
|
||||||
style={{ flex: 1 }}
|
disabled={pushTesting}
|
||||||
disabled={pushTesting}
|
onClick={async () => {
|
||||||
onClick={async () => {
|
setPushTesting(true);
|
||||||
setPushTesting(true);
|
setPushResult(null);
|
||||||
setPushResult(null);
|
try {
|
||||||
try {
|
const { results } = await api.testPush('data');
|
||||||
const { results } = await api.testPush('browser');
|
setPushResult({ ok: true, results, mode: 'data' });
|
||||||
setPushResult({ ok: true, results, mode: 'browser' });
|
} catch (e) {
|
||||||
} catch (e) {
|
setPushResult({ ok: false, error: e.message });
|
||||||
setPushResult({ ok: false, error: e.message });
|
} finally {
|
||||||
} finally {
|
setPushTesting(false);
|
||||||
setPushTesting(false);
|
}
|
||||||
}
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{pushTesting ? 'Sending…' : 'Test (via SW)'}
|
||||||
{pushTesting ? 'Sending…' : 'Test (via Browser)'}
|
</button>
|
||||||
</button>
|
<button
|
||||||
</div>
|
className="btn btn-secondary"
|
||||||
<div style={{ fontSize: 12, color: 'var(--text-tertiary)', lineHeight: 1.4 }}>
|
style={{ flex: 1 }}
|
||||||
<strong>Test (via SW)</strong> — normal production path, service worker shows notification.<br/>
|
disabled={pushTesting}
|
||||||
<strong>Test (via Browser)</strong> — bypasses service worker; Chrome displays directly.
|
onClick={async () => {
|
||||||
</div>
|
setPushTesting(true);
|
||||||
|
setPushResult(null);
|
||||||
|
try {
|
||||||
|
const { results } = await api.testPush('browser');
|
||||||
|
setPushResult({ ok: true, results, mode: 'browser' });
|
||||||
|
} catch (e) {
|
||||||
|
setPushResult({ ok: false, error: e.message });
|
||||||
|
} finally {
|
||||||
|
setPushTesting(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pushTesting ? 'Sending…' : 'Test (via Browser)'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--text-tertiary)', lineHeight: 1.4 }}>
|
||||||
|
<strong>Test (via SW)</strong> — normal production path, service worker shows notification.<br/>
|
||||||
|
<strong>Test (via Browser)</strong> — bypasses service worker; Chrome displays directly.
|
||||||
|
</div>
|
||||||
|
</>)}
|
||||||
{pushResult && (
|
{pushResult && (
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '10px 12px',
|
padding: '10px 12px',
|
||||||
|
|||||||
@@ -121,10 +121,9 @@ export default function Chat() {
|
|||||||
|
|
||||||
const reg = await navigator.serviceWorker.ready;
|
const reg = await navigator.serviceWorker.ready;
|
||||||
|
|
||||||
if (Notification.permission !== 'granted') {
|
// Never auto-request permission — that triggers a dialog on PWA launch.
|
||||||
const granted = await Notification.requestPermission();
|
// Permission is requested explicitly from the Notifications tab in the profile modal.
|
||||||
if (granted !== 'granted') return;
|
if (Notification.permission !== 'granted') return;
|
||||||
}
|
|
||||||
|
|
||||||
// Do NOT call deleteToken() here. Deleting the token on every page load (or
|
// Do NOT call deleteToken() here. Deleting the token on every page load (or
|
||||||
// every visibility-change) forces Chrome to create a new Web Push subscription
|
// every visibility-change) forces Chrome to create a new Web Push subscription
|
||||||
@@ -174,8 +173,13 @@ export default function Chat() {
|
|||||||
const handleVisibility = () => {
|
const handleVisibility = () => {
|
||||||
if (document.visibilityState === 'visible') registerPush();
|
if (document.visibilityState === 'visible') registerPush();
|
||||||
};
|
};
|
||||||
|
const handlePushInit = () => registerPush();
|
||||||
document.addEventListener('visibilitychange', handleVisibility);
|
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.
|
// When a message is deleted, update the sidebar preview immediately.
|
||||||
|
|||||||
Reference in New Issue
Block a user