From ff6743c9b1cccfef9152a07593ad1f2a500a4264 Mon Sep 17 00:00:00 2001 From: Ricky Stretch Date: Sun, 29 Mar 2026 23:21:35 -0400 Subject: [PATCH] v0.12.40 iso notificastion bug fix --- backend/package.json | 2 +- build.sh | 2 +- frontend/package.json | 2 +- frontend/public/sw.js | 8 +++++-- frontend/src/pages/Chat.jsx | 42 ++++++++++++++++++++++++++++++++----- 5 files changed, 46 insertions(+), 10 deletions(-) diff --git a/backend/package.json b/backend/package.json index d79656f..ce2fa3b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "rosterchirp-backend", - "version": "0.12.39", + "version": "0.12.40", "description": "RosterChirp backend server", "main": "src/index.js", "scripts": { diff --git a/build.sh b/build.sh index 5e1229e..b875192 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.12.39}" +VERSION="${1:-0.12.40}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="rosterchirp" diff --git a/frontend/package.json b/frontend/package.json index 467b8b8..4dd5c17 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "rosterchirp-frontend", - "version": "0.12.39", + "version": "0.12.40", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/public/sw.js b/frontend/public/sw.js index fb559b9..00cb9b0 100644 --- a/frontend/public/sw.js +++ b/frontend/public/sw.js @@ -13,9 +13,13 @@ const FIREBASE_CONFIG = { appId: "1:126479377334:web:280abdd135cf7e0c50d717" }; -// Initialise Firebase synchronously so the push listener is ready immediately +// Initialise Firebase synchronously so the push listener is ready immediately. +// Skip on iOS — iOS PWAs use standard W3C WebPush (VAPID), not FCM. Initialising +// firebase.messaging() on iOS registers a second internal push listener alongside +// the custom one below, causing every notification to appear twice. +const isIOS = /iPhone|iPad|iPod/.test(self.navigator?.userAgent || ''); let messaging = null; -if (FIREBASE_CONFIG.apiKey !== '__FIREBASE_API_KEY__') { +if (!isIOS && FIREBASE_CONFIG.apiKey !== '__FIREBASE_API_KEY__') { firebase.initializeApp(FIREBASE_CONFIG); messaging = firebase.messaging(); console.log('[SW] Firebase initialised'); diff --git a/frontend/src/pages/Chat.jsx b/frontend/src/pages/Chat.jsx index d3bed4a..ff99948 100644 --- a/frontend/src/pages/Chat.jsx +++ b/frontend/src/pages/Chat.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { useSocket } from '../contexts/SocketContext.jsx'; import { useAuth } from '../contexts/AuthContext.jsx'; import { useToast } from '../contexts/ToastContext.jsx'; @@ -35,6 +35,9 @@ export default function Chat() { const toast = useToast(); const [groups, setGroups] = useState({ publicGroups: [], privateGroups: [] }); + // Ref so visibility/reconnect handlers always see the latest groups without + // being dependencies of the socket effect (which would cause excessive re-runs) + const groupsRef = useRef({ publicGroups: [], privateGroups: [] }); const [onlineUserIds, setOnlineUserIds] = useState(new Set()); const [activeGroupId, setActiveGroupId] = useState(null); const [chatHasText, setChatHasText] = useState(false); @@ -74,6 +77,9 @@ export default function Chat() { useEffect(() => { loadGroups(); }, [loadGroups]); + // Keep groupsRef in sync so visibility/reconnect handlers can read current groups + useEffect(() => { groupsRef.current = groups; }, [groups]); + // Load feature flags + current user's group memberships on mount const loadFeatures = useCallback(() => { api.getSettings().then(({ settings }) => { @@ -424,14 +430,40 @@ export default function Chat() { socket.on('group:updated', handleGroupUpdated); socket.on('session:displaced', handleSessionDisplaced); - // Bug B fix: on reconnect, reload groups to catch any messages missed while offline - const handleReconnect = () => { loadGroups(); }; + // On reconnect or visibility restore: reload groups AND badge any groups that + // received messages while the iOS PWA was backgrounded (socket was dead, so + // message:new events were never received — only push notifications arrived). + const checkForMissedMessages = () => { + api.getGroups().then(newGroups => { + const prev = groupsRef.current; + setGroups(newGroups); + const allPrev = [...prev.publicGroups, ...prev.privateGroups]; + const allNew = [...newGroups.publicGroups, ...newGroups.privateGroups]; + setUnreadGroups(prevUnread => { + const next = new Map(prevUnread); + for (const ng of allNew) { + if (ng.id === activeGroupId) continue; // currently open — no badge + if (ng.last_message_user_id === user?.id) continue; // own message + const pg = allPrev.find(g => g.id === ng.id); + const isNewer = ng.last_message_at && ( + !pg?.last_message_at || + new Date(ng.last_message_at) > new Date(pg.last_message_at) + ); + if (isNewer && !next.has(ng.id)) { + next.set(ng.id, 1); + } + } + return next; + }); + }).catch(() => {}); + }; + + const handleReconnect = () => { checkForMissedMessages(); }; socket.on('connect', handleReconnect); - // Bug B fix: also reload on visibility restore if socket is already connected const handleVisibility = () => { if (document.visibilityState === 'visible' && socket.connected) { - loadGroups(); + checkForMissedMessages(); } }; document.addEventListener('visibilitychange', handleVisibility);