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 = {