diff --git a/src/js/services/pushFlicIntegration.js b/src/js/services/pushFlicIntegration.js index 310eaa0..6c84359 100644 --- a/src/js/services/pushFlicIntegration.js +++ b/src/js/services/pushFlicIntegration.js @@ -4,6 +4,61 @@ import { getPublicVapidKey, BACKEND_URL, FLIC_BUTTON_ID, FLIC_ACTIONS, FLIC_BATT let pushSubscription = null; // Keep track locally if needed let actionHandlers = {}; // Store handlers for different Flic actions let lastBatteryWarningTimestamp = 0; // Track when last battery warning was shown +const BATTERY_WARNING_STORAGE_KEY = 'last-battery-warning-timestamp'; + +// On initialization, try to load timestamp from localStorage +try { + const storedTimestamp = localStorage.getItem(BATTERY_WARNING_STORAGE_KEY); + if (storedTimestamp) { + lastBatteryWarningTimestamp = parseInt(storedTimestamp, 10); + console.log('[PushFlic] Loaded battery warning timestamp from localStorage:', new Date(lastBatteryWarningTimestamp)); + } +} catch (error) { + console.error('[PushFlic] Error loading timestamp from localStorage:', error); +} + +// Also try to get timestamp from service worker +function syncTimestampWithServiceWorker() { + if ('serviceWorker' in navigator && navigator.serviceWorker.controller) { + // First, register for battery timestamp messages + navigator.serviceWorker.addEventListener('message', event => { + if (event.data && event.data.type === 'battery-timestamp') { + const swTimestamp = event.data.timestamp; + console.log('[PushFlic] Received timestamp from SW:', new Date(swTimestamp)); + + // Use the most recent timestamp (either from SW or local) + if (swTimestamp > lastBatteryWarningTimestamp) { + lastBatteryWarningTimestamp = swTimestamp; + saveTimestampToLocalStorage(swTimestamp); + } else if (lastBatteryWarningTimestamp > swTimestamp) { + // Update the service worker with our more recent timestamp + navigator.serviceWorker.controller.postMessage({ + type: 'update-battery-timestamp', + timestamp: lastBatteryWarningTimestamp + }); + } + } + }); + + // Ask service worker for its timestamp + navigator.serviceWorker.controller.postMessage({ + type: 'get-battery-timestamp' + }); + } +} + +// Call this on init +setTimeout(syncTimestampWithServiceWorker, 1000); + +// Save timestamp to localStorage +function saveTimestampToLocalStorage(timestamp) { + try { + localStorage.setItem(BATTERY_WARNING_STORAGE_KEY, timestamp.toString()); + console.log('[PushFlic] Saved timestamp to localStorage:', new Date(timestamp)); + } catch (error) { + console.error('[PushFlic] Error saving timestamp to localStorage:', error); + } +} // --- Helper Functions --- @@ -79,8 +134,20 @@ function showBatteryWarning(batteryLevel) { return; } + // Update timestamp lastBatteryWarningTimestamp = now; + // Save to localStorage + saveTimestampToLocalStorage(now); + + // Also update service worker + if ('serviceWorker' in navigator && navigator.serviceWorker.controller) { + navigator.serviceWorker.controller.postMessage({ + type: 'update-battery-timestamp', + timestamp: now + }); + } + // Show the notification console.log(`[PushFlic] Low battery detected: ${batteryLevel}%`); @@ -328,6 +395,9 @@ export function initPushFlic(handlers) { console.warn('[PushFlic] No action handlers provided to initPushFlic, actions will not work!'); } + // Sync battery timestamp with service worker + syncTimestampWithServiceWorker(); + // Auto-subscribe when permission is granted if ('serviceWorker' in navigator) { navigator.serviceWorker.ready.then(registration => { diff --git a/sw.js b/sw.js index d1e9086..f881eeb 100644 --- a/sw.js +++ b/sw.js @@ -2,6 +2,56 @@ const CACHE_VERSION = 'v1.0.2'; const CACHE_NAME = `game-timer-${CACHE_VERSION}`; +// Store last battery warning timestamp to prevent repeated notifications +let lastBatteryWarningTimestamp = 0; +const FOUR_HOURS_MS = 4 * 60 * 60 * 1000; // 4 hours in milliseconds + +// Settings cache for storing timestamps +const SETTINGS_CACHE = 'settings-cache-v1'; +const TIMESTAMP_URL = new Request('/_timestamp/battery-warning'); + +// Function to load the timestamp from cache +async function loadTimestamp() { + try { + const cache = await caches.open(SETTINGS_CACHE); + const response = await cache.match(TIMESTAMP_URL); + + if (response) { + const data = await response.json(); + lastBatteryWarningTimestamp = data.timestamp; + console.log('[ServiceWorker] Loaded battery warning timestamp:', new Date(lastBatteryWarningTimestamp)); + } else { + console.log('[ServiceWorker] No saved timestamp found'); + } + return true; + } catch (error) { + console.error('[ServiceWorker] Error loading timestamp:', error); + return false; + } +} + +// Function to save the timestamp to cache +async function saveTimestamp(timestamp) { + try { + const cache = await caches.open(SETTINGS_CACHE); + const response = new Response(JSON.stringify({ timestamp }), { + headers: { 'Content-Type': 'application/json' } + }); + + await cache.put(TIMESTAMP_URL, response); + console.log('[ServiceWorker] Saved battery warning timestamp:', new Date(timestamp)); + return true; + } catch (error) { + console.error('[ServiceWorker] Error saving timestamp:', error); + return false; + } +} + +// Initialize and load data when the service worker starts +loadTimestamp().then(success => { + console.log('[ServiceWorker] Timestamp loading ' + (success ? 'successful' : 'failed')); +}); + // Files to cache const CACHE_FILES = [ '/', @@ -136,14 +186,32 @@ self.addEventListener('push', event => { console.log('[ServiceWorker] Preparing message payload:', messagePayload); // Check if this is a low battery alert that needs a notification - const isBatteryAlert = messagePayload.batteryLevel !== undefined && - messagePayload.batteryLevel < 50; // Use the same threshold as in the app + const batteryLevel = messagePayload.batteryLevel; + const isBatteryLow = batteryLevel !== undefined && batteryLevel < 50; // Use the same threshold as in the app - if (isBatteryAlert) { - console.log(`[ServiceWorker] Low battery alert detected: ${messagePayload.batteryLevel}%`); - // Change notification title/body for battery alerts - pushData.title = 'Flic Button Low Battery'; - pushData.body = `Battery level is ${messagePayload.batteryLevel}%. Please replace batteries soon.`; + // Determine if we should show a battery notification (throttle to once every 4 hours) + let shouldShowBatteryNotification = false; + + if (isBatteryLow) { + const now = Date.now(); + if (now - lastBatteryWarningTimestamp > FOUR_HOURS_MS) { + // It's been more than 4 hours since the last battery notification + console.log(`[ServiceWorker] Low battery (${batteryLevel}%) - showing notification`); + lastBatteryWarningTimestamp = now; + + // Save the timestamp to cache + saveTimestamp(now).catch(error => { + console.warn('[ServiceWorker] Failed to save battery warning timestamp:', error); + }); + + shouldShowBatteryNotification = true; + + // Change notification title/body for battery alerts + pushData.title = 'Flic Button Low Battery'; + pushData.body = `Battery level is ${batteryLevel}%. Please replace batteries soon.`; + } else { + console.log(`[ServiceWorker] Low battery (${batteryLevel}%) - suppressing notification (shown recently)`); + } } // Send message to all open PWA windows controlled by this SW @@ -154,8 +222,8 @@ self.addEventListener('push', event => { }).then(clientList => { if (!clientList || clientList.length === 0) { console.log('[ServiceWorker] No client windows found to send message to.'); - // If no window is open AND this is a battery alert, show a notification - if (isBatteryAlert) { + // If no window is open AND this is a battery alert that should be shown, show a notification + if (shouldShowBatteryNotification) { return self.registration.showNotification(pushData.title, { body: pushData.body, icon: '/icons/android-chrome-192x192.png', // Updated path @@ -184,21 +252,48 @@ self.addEventListener('push', event => { }); return Promise.all(sendPromises).then(() => { - // Only show a notification if this is a battery alert - if (isBatteryAlert) { + // Only show a notification if this is a battery alert that should be shown + if (shouldShowBatteryNotification) { return self.registration.showNotification(pushData.title, { body: pushData.body, icon: '/icons/android-chrome-192x192.png', data: pushData.data }); } - // For regular button presses, don't show notifications + // For regular button presses or throttled battery alerts, don't show notifications return Promise.resolve(); }); }) ); }); +// Listen for messages from client +self.addEventListener('message', event => { + const message = event.data; + + if (!message || typeof message !== 'object') { + return; + } + + // Handle get timestamp request + if (message.type === 'get-battery-timestamp') { + console.log('[ServiceWorker] Client requested battery warning timestamp'); + event.source.postMessage({ + type: 'battery-timestamp', + timestamp: lastBatteryWarningTimestamp + }); + } + + // Handle update timestamp request + if (message.type === 'update-battery-timestamp' && message.timestamp) { + console.log('[ServiceWorker] Updating battery warning timestamp to:', new Date(message.timestamp)); + lastBatteryWarningTimestamp = message.timestamp; + saveTimestamp(message.timestamp).catch(error => { + console.warn('[ServiceWorker] Failed to save updated battery warning timestamp:', error); + }); + } +}); + // This helps with navigation after app is installed self.addEventListener('notificationclick', event => { console.log('[ServiceWorker] Notification click received');