diff --git a/index.html b/index.html index a62a3b7..99e0cbe 100644 --- a/index.html +++ b/index.html @@ -1177,6 +1177,16 @@ let ws = null; let userId = null; let otherUsers = new Map(); + + // Notification cooldown tracking + let notificationCooldowns = { + nearbyCache: {}, // cacheId -> lastNotificationTime + destinationArrival: 0 // lastNotificationTime + }; + const CACHE_COOLDOWN = 10 * 60 * 1000; // 10 minutes + const CACHE_NOTIFY_DISTANCE = 200; // meters + const CACHE_RESET_DISTANCE = 200; // meters to reset cooldown + const DESTINATION_ARRIVAL_DISTANCE = 10; // meters let wsReconnectTimer = null; let myIcon = null; let myColor = null; @@ -1407,6 +1417,9 @@ // Send location to other users with visibility info sendLocationToServer(lat, lng, accuracy, isNearTrack); + // Check for notification triggers + checkLocationNotifications(lat, lng); + // Check geocache proximity checkGeocacheProximity(); @@ -2540,8 +2553,28 @@ if (data.geocache) { const existingIndex = geocaches.findIndex(g => g.id === data.geocache.id); if (existingIndex >= 0) { + // Check if this is a new message from another user + const oldCache = geocaches[existingIndex]; + const newMessagesCount = data.geocache.messages.length - oldCache.messages.length; + // Update existing geocache geocaches[existingIndex] = data.geocache; + + // Send notification if new message added by another user and we're nearby + if (newMessagesCount > 0 && userLocation) { + const distance = L.latLng(userLocation.lat, userLocation.lng) + .distanceTo(L.latLng(data.geocache.lat, data.geocache.lng)); + + if (distance <= CACHE_NOTIFY_DISTANCE) { + const latestMessage = data.geocache.messages[data.geocache.messages.length - 1]; + sendPushNotification( + '💬 New Cache Message', + `${latestMessage.name}: ${latestMessage.text.substring(0, 50)}...`, + 'cacheMessage' + ); + } + } + // Refresh dialog if it's open for this geocache if (currentGeocache && currentGeocache.id === data.geocache.id) { showGeocacheDialog(data.geocache); @@ -2620,6 +2653,83 @@ } } + async function checkLocationNotifications(lat, lng) { + if (!pushSubscription) return; // No push notifications enabled + + const now = Date.now(); + const userPos = L.latLng(lat, lng); + + // 1. Check for nearby geocaches + geocaches.forEach(cache => { + if (!cache || !cache.lat || !cache.lng) return; + + const cachePos = L.latLng(cache.lat, cache.lng); + const distance = userPos.distanceTo(cachePos); + + // Check if we should notify about this cache + const lastNotified = notificationCooldowns.nearbyCache[cache.id] || 0; + const timeSinceNotification = now - lastNotified; + + if (distance <= CACHE_NOTIFY_DISTANCE) { + // Within notification distance + if (timeSinceNotification > CACHE_COOLDOWN || lastNotified === 0) { + // Send notification + sendPushNotification( + '📍 Geocache Nearby!', + `"${cache.title}" is ${Math.round(distance)}m away`, + 'nearbyCache' + ); + notificationCooldowns.nearbyCache[cache.id] = now; + } + } else if (distance > CACHE_RESET_DISTANCE && lastNotified > 0) { + // Reset cooldown if we've moved far enough away + delete notificationCooldowns.nearbyCache[cache.id]; + } + }); + + // 2. Check for destination arrival (only in nav mode) + if (navMode && destinationPin) { + const destPos = destinationPin.getLatLng(); + const distance = userPos.distanceTo(destPos); + + if (distance <= DESTINATION_ARRIVAL_DISTANCE) { + const timeSinceNotification = now - notificationCooldowns.destinationArrival; + if (timeSinceNotification > 60000) { // 1 minute cooldown for arrival + sendPushNotification( + '🎯 Destination Reached!', + 'You have arrived at your destination', + 'destinationArrival' + ); + notificationCooldowns.destinationArrival = now; + } + } + } + } + + async function sendPushNotification(title, body, type) { + try { + // Send to server to trigger push notification + const response = await fetch('/send-notification', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + title: title, + body: body, + type: type, + userId: userId // Send to self + }) + }); + + if (!response.ok) { + console.error('Failed to send push notification'); + } + } catch (error) { + console.error('Error sending push notification:', error); + } + } + function updateOtherUser(userId, lat, lng, accuracy, icon, color) { let userMarker = otherUsers.get(userId); diff --git a/server.js b/server.js index 3b5ea12..993e7d7 100644 --- a/server.js +++ b/server.js @@ -209,6 +209,52 @@ app.post('/unsubscribe', async (req, res) => { } }); +// Send notification to a specific user +app.post('/send-notification', async (req, res) => { + try { + const { title, body, type, userId } = req.body; + + if (!pushSubscriptions.length) { + return res.json({ success: false, message: 'No subscriptions' }); + } + + const payload = { + title: title || 'HikeMap Notification', + body: body || '', + icon: '/icon-192x192.png', + badge: '/icon-72x72.png', + data: { + type: type, + timestamp: Date.now() + } + }; + + // Send to all subscriptions (in a real app, filter by userId) + const results = await Promise.allSettled( + pushSubscriptions.map(subscription => + webpush.sendNotification(subscription, JSON.stringify(payload)) + .catch(err => { + if (err.statusCode === 410) { + // Subscription expired, remove it + pushSubscriptions = pushSubscriptions.filter(sub => + sub.endpoint !== subscription.endpoint + ); + } + throw err; + }) + ) + ); + + const successful = results.filter(r => r.status === 'fulfilled').length; + console.log(`Sent notification to ${successful}/${pushSubscriptions.length} subscribers`); + + res.json({ success: true, sent: successful }); + } catch (err) { + console.error('Error sending notification:', err); + res.status(500).json({ error: 'Failed to send notification' }); + } +}); + // Function to send push notification to all subscribers async function sendPushNotification(title, body, data = {}) { const notification = {