Browse Source

Add location-based push notification system

Implemented three types of notifications with cooldowns:

1. Nearby Cache Notification
   - Triggers when within 200m of a geocache
   - 10-minute cooldown per cache
   - Resets when you move >200m away

2. Cache Message Notification
   - Triggers when another user adds a message to a nearby cache
   - Only notifies if you're within 200m of the cache

3. Destination Arrival Notification
   - Triggers when within 10m of navigation destination
   - 1-minute cooldown to prevent spam

Features:
- Server endpoint /send-notification for triggering push notifications
- Cooldown tracking to prevent notification spam
- Distance-based triggers for relevant notifications
- Automatic cleanup of expired subscriptions

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
master
HikeMap User 1 month ago
parent
commit
ce984cf22b
  1. 110
      index.html
  2. 46
      server.js

110
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);

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

Loading…
Cancel
Save