v0.12.40 iso notificastion bug fix

This commit is contained in:
2026-03-29 23:21:35 -04:00
parent d03baec163
commit ff6743c9b1
5 changed files with 46 additions and 10 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "rosterchirp-backend", "name": "rosterchirp-backend",
"version": "0.12.39", "version": "0.12.40",
"description": "RosterChirp backend server", "description": "RosterChirp backend server",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {

View File

@@ -13,7 +13,7 @@
# ───────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────
set -euo pipefail set -euo pipefail
VERSION="${1:-0.12.39}" VERSION="${1:-0.12.40}"
ACTION="${2:-}" ACTION="${2:-}"
REGISTRY="${REGISTRY:-}" REGISTRY="${REGISTRY:-}"
IMAGE_NAME="rosterchirp" IMAGE_NAME="rosterchirp"

View File

@@ -1,6 +1,6 @@
{ {
"name": "rosterchirp-frontend", "name": "rosterchirp-frontend",
"version": "0.12.39", "version": "0.12.40",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -13,9 +13,13 @@ const FIREBASE_CONFIG = {
appId: "1:126479377334:web:280abdd135cf7e0c50d717" 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; let messaging = null;
if (FIREBASE_CONFIG.apiKey !== '__FIREBASE_API_KEY__') { if (!isIOS && FIREBASE_CONFIG.apiKey !== '__FIREBASE_API_KEY__') {
firebase.initializeApp(FIREBASE_CONFIG); firebase.initializeApp(FIREBASE_CONFIG);
messaging = firebase.messaging(); messaging = firebase.messaging();
console.log('[SW] Firebase initialised'); console.log('[SW] Firebase initialised');

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback, useRef } from 'react';
import { useSocket } from '../contexts/SocketContext.jsx'; import { useSocket } from '../contexts/SocketContext.jsx';
import { useAuth } from '../contexts/AuthContext.jsx'; import { useAuth } from '../contexts/AuthContext.jsx';
import { useToast } from '../contexts/ToastContext.jsx'; import { useToast } from '../contexts/ToastContext.jsx';
@@ -35,6 +35,9 @@ export default function Chat() {
const toast = useToast(); const toast = useToast();
const [groups, setGroups] = useState({ publicGroups: [], privateGroups: [] }); 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 [onlineUserIds, setOnlineUserIds] = useState(new Set());
const [activeGroupId, setActiveGroupId] = useState(null); const [activeGroupId, setActiveGroupId] = useState(null);
const [chatHasText, setChatHasText] = useState(false); const [chatHasText, setChatHasText] = useState(false);
@@ -74,6 +77,9 @@ export default function Chat() {
useEffect(() => { loadGroups(); }, [loadGroups]); 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 // Load feature flags + current user's group memberships on mount
const loadFeatures = useCallback(() => { const loadFeatures = useCallback(() => {
api.getSettings().then(({ settings }) => { api.getSettings().then(({ settings }) => {
@@ -424,14 +430,40 @@ export default function Chat() {
socket.on('group:updated', handleGroupUpdated); socket.on('group:updated', handleGroupUpdated);
socket.on('session:displaced', handleSessionDisplaced); socket.on('session:displaced', handleSessionDisplaced);
// Bug B fix: on reconnect, reload groups to catch any messages missed while offline // On reconnect or visibility restore: reload groups AND badge any groups that
const handleReconnect = () => { loadGroups(); }; // 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); socket.on('connect', handleReconnect);
// Bug B fix: also reload on visibility restore if socket is already connected
const handleVisibility = () => { const handleVisibility = () => {
if (document.visibilityState === 'visible' && socket.connected) { if (document.visibilityState === 'visible' && socket.connected) {
loadGroups(); checkForMissedMessages();
} }
}; };
document.addEventListener('visibilitychange', handleVisibility); document.addEventListener('visibilitychange', handleVisibility);