feat: Grey out stale locations, remove indoor map, dedupe pings

1. Stale locations (>5 min old) are now greyed out on the map with
   reduced opacity and hover tooltip showing time since last seen
2. Removed indoor map button and related UI elements
3. Location request pings now deduplicate by participant name to
   avoid pinging the same user multiple times

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-01-02 13:06:52 +01:00
parent 007a7e877f
commit ff45193ba2
3 changed files with 51 additions and 32 deletions

View File

@ -209,38 +209,11 @@ export default function DualMapView({
</button>
)}
{/* Indoor Map button - switch to indoor view */}
{activeView === 'outdoor' && (
<button
onClick={goIndoor}
className="absolute bottom-4 left-4 bg-rmaps-dark/90 text-white px-3 py-2 rounded-lg text-sm hover:bg-rmaps-dark transition-colors flex items-center gap-2 z-30"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
/>
</svg>
Indoor Map
</button>
)}
{/* Auto-mode indicator */}
{mode === 'auto' && (
<div className="absolute top-4 left-4 bg-rmaps-primary/20 text-rmaps-primary text-xs px-2 py-1 rounded-full">
Auto-detecting location
</div>
)}
{/* Venue proximity indicator */}
{currentLocation && isInC3NavArea(currentLocation.latitude, currentLocation.longitude) && activeView === 'outdoor' && (
<div className="absolute top-4 left-1/2 -translate-x-1/2 bg-rmaps-secondary/90 text-white text-sm px-3 py-1.5 rounded-full flex items-center gap-2 z-30">
<div className="absolute top-4 left-1/2 -translate-x-1/2 bg-rmaps-secondary/90 text-white text-sm px-3 py-1.5 rounded-full z-30">
<span>You&apos;re at the venue!</span>
<button onClick={goIndoor} className="underline">
Switch to indoor
</button>
</div>
)}
</div>

View File

@ -57,6 +57,12 @@ function isValidCoordinate(lat: number, lng: number): boolean {
);
}
// Check if a participant's location is stale (older than 5 minutes)
const STALE_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes
function isLocationStale(lastSeen: number): boolean {
return Date.now() - lastSeen > STALE_THRESHOLD_MS;
}
export default function MapView({
participants,
waypoints = [],
@ -202,15 +208,36 @@ export default function MapView({
let marker = currentMarkers.get(participant.id);
if (marker) {
// Update existing marker position
// Update existing marker position and stale status
marker.setLngLat([longitude, latitude]);
const el = marker.getElement();
const isStale = isLocationStale(participant.lastSeen.getTime());
if (isStale) {
el.style.backgroundColor = '#6b7280';
el.style.opacity = '0.6';
el.title = `${participant.name} - last seen ${Math.round((Date.now() - participant.lastSeen.getTime()) / 60000)} min ago`;
} else {
el.style.backgroundColor = participant.color;
el.style.opacity = '1';
el.title = participant.name;
}
} else {
// Create new marker
const el = document.createElement('div');
el.className = 'friend-marker';
el.style.backgroundColor = participant.color;
el.innerHTML = participant.emoji;
const isStale = isLocationStale(participant.lastSeen.getTime());
if (isStale) {
// Grey out stale locations
el.style.backgroundColor = '#6b7280'; // gray-500
el.style.opacity = '0.6';
el.title = `${participant.name} - last seen ${Math.round((Date.now() - participant.lastSeen.getTime()) / 60000)} min ago`;
} else {
el.style.backgroundColor = participant.color;
}
if (participant.id === currentUserId) {
el.classList.add('sharing');
}

View File

@ -485,15 +485,34 @@ const server = createServer(async (req, res) => {
let pushSent = 0;
let pushFailed = 0;
// First, send WebSocket message to all connected clients in the room
// Get room to access participant names for deduplication
const room = rooms.get(roomSlug);
// First, send WebSocket message to connected clients, deduplicated by name
const locationRequestMsg = JSON.stringify({ type: 'request_location' });
const pingedNames = new Set();
for (const [ws, clientInfo] of clients.entries()) {
if (clientInfo.roomSlug === roomSlug && ws.readyState === 1) {
// Get participant name for this client
const participant = room?.participants?.[clientInfo.participantId];
const name = participant?.name;
// Skip if we've already pinged this name
if (name && pingedNames.has(name)) {
console.log(`[${roomSlug}] Skipping duplicate ping for: ${name}`);
continue;
}
ws.send(locationRequestMsg);
wsSent++;
if (name) {
pingedNames.add(name);
}
}
console.log(`[${roomSlug}] Location request via WebSocket: ${wsSent} clients`);
}
console.log(`[${roomSlug}] Location request via WebSocket: ${wsSent} clients (deduped by name)`);
// Then, send push notifications to offline subscribers
const subs = pushSubscriptions.get(roomSlug);