fix: deduplicate participants, visible callout push for offline users, fix notification timing
- Server-side participant dedup on join: remove ghost entries with same name but different ID
- Reduce stale participant threshold from 1hr to 15min to match client-side cleanup
- Refactor push subscriptions from Set to Map keyed by endpoint (prevents duplicate pushes)
- Store participantId with push subscriptions for identity-aware routing
- Exclude joining user from their own "Friend Joined" push notification
- Callout (ping) sends visible push to offline users ("X is looking for you!") instead of silent push
- Return last known locations in callout API response for immediate display
- Service worker: 10s cooldown on location request pushes to prevent burst on app reopen
- Service worker: suppress join/leave notifications when app window is focused
- Pass callerName from ParticipantList so offline callout shows who's looking
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8e96d4eec0
commit
30f32e6da7
|
|
@ -13,3 +13,4 @@ auto_commit: false
|
|||
bypass_git_hooks: false
|
||||
check_active_branches: true
|
||||
active_branch_days: 30
|
||||
task_prefix: "task"
|
||||
|
|
|
|||
54
public/sw.js
54
public/sw.js
|
|
@ -297,6 +297,11 @@ async function requestLocationFromClient() {
|
|||
}
|
||||
|
||||
// ==================== PUSH NOTIFICATIONS ====================
|
||||
|
||||
// Cooldown to prevent burst of queued location requests when app reopens
|
||||
let lastLocationRequestTime = 0;
|
||||
const LOCATION_REQUEST_COOLDOWN_MS = 10000; // 10 seconds
|
||||
|
||||
self.addEventListener('push', (event) => {
|
||||
console.log('[SW] Push received:', event);
|
||||
|
||||
|
|
@ -321,6 +326,13 @@ self.addEventListener('push', (event) => {
|
|||
|
||||
// Handle silent push - request location update without showing notification
|
||||
if (data.silent || data.data?.type === 'location_request') {
|
||||
const now = Date.now();
|
||||
if (now - lastLocationRequestTime < LOCATION_REQUEST_COOLDOWN_MS) {
|
||||
console.log('[SW] Skipping duplicate location request (cooldown)');
|
||||
return;
|
||||
}
|
||||
lastLocationRequestTime = now;
|
||||
|
||||
event.waitUntil(
|
||||
requestLocationFromClient().then((sent) => {
|
||||
console.log('[SW] Silent push - location request sent:', sent);
|
||||
|
|
@ -329,21 +341,35 @@ self.addEventListener('push', (event) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
body: data.body,
|
||||
icon: data.icon || '/icon-192.png',
|
||||
badge: data.badge || '/icon-192.png',
|
||||
tag: data.tag || 'rmaps-notification',
|
||||
data: data.data || {},
|
||||
vibrate: [200, 100, 200, 100, 400],
|
||||
actions: data.actions || [],
|
||||
requireInteraction: data.requireInteraction || false,
|
||||
silent: false,
|
||||
renotify: true,
|
||||
};
|
||||
|
||||
// For join/leave notifications, suppress if app is currently focused
|
||||
// (user can already see who's joining/leaving)
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(data.title, options)
|
||||
(async () => {
|
||||
const notifType = data.data?.type;
|
||||
if (notifType === 'join' || notifType === 'leave') {
|
||||
const allClients = await self.clients.matchAll({ type: 'window' });
|
||||
const hasFocusedClient = allClients.some(c => c.visibilityState === 'visible');
|
||||
if (hasFocusedClient) {
|
||||
console.log('[SW] Suppressing join/leave notification - app is focused');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const options = {
|
||||
body: data.body,
|
||||
icon: data.icon || '/icon-192.png',
|
||||
badge: data.badge || '/icon-192.png',
|
||||
tag: data.tag || 'rmaps-notification',
|
||||
data: data.data || {},
|
||||
vibrate: [200, 100, 200, 100, 400],
|
||||
actions: data.actions || [],
|
||||
requireInteraction: data.requireInteraction || false,
|
||||
silent: false,
|
||||
renotify: true,
|
||||
};
|
||||
|
||||
await self.registration.showNotification(data.title, options);
|
||||
})()
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -165,6 +165,7 @@ export default function RoomPage() {
|
|||
const { subscribe: subscribePush, isSubscribed: isPushSubscribed } = usePushNotifications({
|
||||
syncUrl,
|
||||
roomSlug: slug,
|
||||
participantId: currentParticipantId || undefined,
|
||||
});
|
||||
|
||||
// Auto-subscribe to push when joining room (like location permission)
|
||||
|
|
@ -377,6 +378,7 @@ export default function RoomPage() {
|
|||
<ParticipantList
|
||||
participants={participants}
|
||||
currentUserId={currentUser.name}
|
||||
currentUserName={currentUser.name}
|
||||
roomSlug={slug}
|
||||
syncUrl={process.env.NEXT_PUBLIC_SYNC_URL}
|
||||
onClose={() => setShowParticipants(false)}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type { Participant } from '@/types';
|
|||
interface ParticipantListProps {
|
||||
participants: Participant[];
|
||||
currentUserId?: string;
|
||||
currentUserName?: string;
|
||||
roomSlug: string;
|
||||
syncUrl?: string;
|
||||
onClose: () => void;
|
||||
|
|
@ -17,6 +18,7 @@ interface ParticipantListProps {
|
|||
export default function ParticipantList({
|
||||
participants,
|
||||
currentUserId,
|
||||
currentUserName,
|
||||
roomSlug,
|
||||
syncUrl,
|
||||
onClose,
|
||||
|
|
@ -40,7 +42,7 @@ export default function ParticipantList({
|
|||
const response = await fetch(`${httpUrl}/push/request-location`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ roomSlug }),
|
||||
body: JSON.stringify({ roomSlug, callerName: currentUserName }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
|
@ -59,7 +61,7 @@ export default function ParticipantList({
|
|||
setIsRefreshing(false);
|
||||
setTimeout(() => setRefreshMessage(null), 3000);
|
||||
}
|
||||
}, [syncUrl, roomSlug, isRefreshing]);
|
||||
}, [syncUrl, roomSlug, isRefreshing, currentUserName]);
|
||||
|
||||
// Ping a single user
|
||||
const handlePingUser = useCallback(async (participantId: string, participantName: string) => {
|
||||
|
|
@ -73,7 +75,7 @@ export default function ParticipantList({
|
|||
const response = await fetch(`${httpUrl}/push/request-location`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ roomSlug, participantId }),
|
||||
body: JSON.stringify({ roomSlug, participantId, callerName: currentUserName }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
|
@ -89,7 +91,7 @@ export default function ParticipantList({
|
|||
setPingingUser(null);
|
||||
setTimeout(() => setRefreshMessage(null), 3000);
|
||||
}
|
||||
}, [syncUrl, roomSlug, pingingUser]);
|
||||
}, [syncUrl, roomSlug, pingingUser, currentUserName]);
|
||||
const formatDistance = (participant: Participant, current: Participant | undefined) => {
|
||||
if (!participant.location || !current?.location) return null;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ interface UsePushNotificationsOptions {
|
|||
roomSlug?: string;
|
||||
/** Sync server URL for storing subscriptions */
|
||||
syncUrl?: string;
|
||||
/** Participant ID to associate with this push subscription */
|
||||
participantId?: string;
|
||||
}
|
||||
|
||||
// VAPID public key - should match the server's public key
|
||||
|
|
@ -37,7 +39,7 @@ function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
|||
}
|
||||
|
||||
export function usePushNotifications(options: UsePushNotificationsOptions = {}) {
|
||||
const { roomSlug, syncUrl } = options;
|
||||
const { roomSlug, syncUrl, participantId } = options;
|
||||
|
||||
const [state, setState] = useState<PushSubscriptionState>({
|
||||
isSupported: false,
|
||||
|
|
@ -162,6 +164,7 @@ export function usePushNotifications(options: UsePushNotificationsOptions = {})
|
|||
body: JSON.stringify({
|
||||
subscription: pushSubscription.toJSON(),
|
||||
roomSlug,
|
||||
participantId,
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -194,7 +197,7 @@ export function usePushNotifications(options: UsePushNotificationsOptions = {})
|
|||
}));
|
||||
throw error;
|
||||
}
|
||||
}, [state.isSupported, syncUrl, roomSlug]);
|
||||
}, [state.isSupported, syncUrl, roomSlug, participantId]);
|
||||
|
||||
// Unsubscribe from push notifications
|
||||
const unsubscribe = useCallback(async () => {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import webpush from 'web-push';
|
|||
import { verifyToken, extractTokenFromURL } from './verify-token.js';
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
const STALE_THRESHOLD_MS = 60 * 60 * 1000; // 1 hour
|
||||
const STALE_THRESHOLD_MS = 15 * 60 * 1000; // 15 minutes (match client-side cleanup)
|
||||
const LOCATION_REQUEST_INTERVAL_MS = parseInt(process.env.LOCATION_REQUEST_INTERVAL || '60000', 10); // 60 seconds default
|
||||
|
||||
// VAPID keys for push notifications
|
||||
|
|
@ -29,7 +29,8 @@ const rooms = new Map();
|
|||
// Client tracking: Map<WebSocket, { roomSlug, participantId, claims, readOnly }>
|
||||
const clients = new Map();
|
||||
|
||||
// Push subscriptions: Map<roomSlug, Set<subscription>>
|
||||
// Push subscriptions: Map<roomSlug, Map<endpoint, { subscription, participantId }>>
|
||||
// Using Map keyed by endpoint to deduplicate subscriptions from the same device
|
||||
const pushSubscriptions = new Map();
|
||||
|
||||
function getRoomState(slug) {
|
||||
|
|
@ -119,38 +120,36 @@ function broadcast(roomSlug, message, excludeWs = null) {
|
|||
}
|
||||
|
||||
// Send push notification to all subscriptions for a room
|
||||
async function sendPushToRoom(roomSlug, notification, excludeEndpoint = null) {
|
||||
// excludeParticipantId: skip subscriptions belonging to this participant (e.g. don't notify yourself)
|
||||
async function sendPushToRoom(roomSlug, notification, { excludeEndpoint = null, excludeParticipantId = null } = {}) {
|
||||
if (!VAPID_PUBLIC_KEY || !VAPID_PRIVATE_KEY) return;
|
||||
|
||||
const subs = pushSubscriptions.get(roomSlug);
|
||||
if (!subs || subs.size === 0) return;
|
||||
const subsMap = pushSubscriptions.get(roomSlug);
|
||||
if (!subsMap || subsMap.size === 0) return;
|
||||
|
||||
const payload = JSON.stringify(notification);
|
||||
const failedEndpoints = [];
|
||||
|
||||
for (const sub of subs) {
|
||||
if (excludeEndpoint && sub.endpoint === excludeEndpoint) continue;
|
||||
for (const [endpoint, { subscription, participantId }] of subsMap.entries()) {
|
||||
if (excludeEndpoint && endpoint === excludeEndpoint) continue;
|
||||
if (excludeParticipantId && participantId === excludeParticipantId) continue;
|
||||
|
||||
try {
|
||||
await webpush.sendNotification(sub, payload);
|
||||
console.log(`[${roomSlug}] Push sent to ${sub.endpoint.slice(-20)}`);
|
||||
await webpush.sendNotification(subscription, payload);
|
||||
console.log(`[${roomSlug}] Push sent to ${endpoint.slice(-20)}`);
|
||||
} catch (error) {
|
||||
console.error(`[${roomSlug}] Push failed:`, error.statusCode || error.message);
|
||||
// Remove invalid subscriptions
|
||||
if (error.statusCode === 404 || error.statusCode === 410) {
|
||||
failedEndpoints.push(sub.endpoint);
|
||||
failedEndpoints.push(endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up failed subscriptions
|
||||
for (const endpoint of failedEndpoints) {
|
||||
for (const sub of subs) {
|
||||
if (sub.endpoint === endpoint) {
|
||||
subs.delete(sub);
|
||||
console.log(`[${roomSlug}] Removed invalid subscription`);
|
||||
}
|
||||
}
|
||||
subsMap.delete(endpoint);
|
||||
console.log(`[${roomSlug}] Removed invalid subscription`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -181,6 +180,16 @@ function handleMessage(ws, data) {
|
|||
...message.participant,
|
||||
lastSeen: Date.now()
|
||||
};
|
||||
|
||||
// Deduplicate: remove any existing participant with same name but different ID (ghost entries)
|
||||
for (const [existingId, existing] of Object.entries(room.participants)) {
|
||||
if (existing.name === participant.name && existingId !== participant.id) {
|
||||
console.log(`[${clientInfo.roomSlug}] Removing ghost participant: ${existingId} (replaced by ${participant.id})`);
|
||||
delete room.participants[existingId];
|
||||
broadcast(clientInfo.roomSlug, { type: 'leave', participantId: existingId }, ws);
|
||||
}
|
||||
}
|
||||
|
||||
room.participants[participant.id] = participant;
|
||||
clientInfo.participantId = participant.id;
|
||||
|
||||
|
|
@ -189,7 +198,7 @@ function handleMessage(ws, data) {
|
|||
// Broadcast join to others
|
||||
broadcast(clientInfo.roomSlug, message, ws);
|
||||
|
||||
// Send push notification to others
|
||||
// Send push notification to others (exclude the joining user themselves)
|
||||
sendPushToRoom(clientInfo.roomSlug, {
|
||||
title: 'Friend Joined! 👋',
|
||||
body: `${participant.name} ${participant.emoji} joined the room`,
|
||||
|
|
@ -200,7 +209,7 @@ function handleMessage(ws, data) {
|
|||
participantId: participant.id,
|
||||
participantName: participant.name,
|
||||
},
|
||||
});
|
||||
}, { excludeParticipantId: participant.id });
|
||||
|
||||
// Send current state to the new participant
|
||||
ws.send(JSON.stringify({
|
||||
|
|
@ -342,37 +351,33 @@ setInterval(() => {
|
|||
async function requestLocationFromAllRooms() {
|
||||
if (!VAPID_PUBLIC_KEY || !VAPID_PRIVATE_KEY) return;
|
||||
|
||||
for (const [roomSlug, subs] of pushSubscriptions.entries()) {
|
||||
if (subs.size === 0) continue;
|
||||
for (const [roomSlug, subsMap] of pushSubscriptions.entries()) {
|
||||
if (subsMap.size === 0) continue;
|
||||
|
||||
const room = rooms.get(roomSlug);
|
||||
// Only request if room has participants (active room)
|
||||
if (!room || Object.keys(room.participants).length === 0) continue;
|
||||
|
||||
console.log(`[${roomSlug}] Requesting location from ${subs.size} subscribers`);
|
||||
console.log(`[${roomSlug}] Requesting location from ${subsMap.size} subscribers`);
|
||||
|
||||
const failedEndpoints = [];
|
||||
for (const sub of subs) {
|
||||
for (const [endpoint, { subscription }] of subsMap.entries()) {
|
||||
try {
|
||||
await webpush.sendNotification(sub, JSON.stringify({
|
||||
await webpush.sendNotification(subscription, JSON.stringify({
|
||||
silent: true,
|
||||
data: { type: 'location_request', roomSlug }
|
||||
}));
|
||||
} catch (error) {
|
||||
if (error.statusCode === 404 || error.statusCode === 410) {
|
||||
failedEndpoints.push(sub.endpoint);
|
||||
failedEndpoints.push(endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up failed subscriptions
|
||||
for (const endpoint of failedEndpoints) {
|
||||
for (const sub of subs) {
|
||||
if (sub.endpoint === endpoint) {
|
||||
subs.delete(sub);
|
||||
console.log(`[${roomSlug}] Removed stale push subscription`);
|
||||
}
|
||||
}
|
||||
subsMap.delete(endpoint);
|
||||
console.log(`[${roomSlug}] Removed stale push subscription`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -433,7 +438,7 @@ const server = createServer(async (req, res) => {
|
|||
roomStats[slug] = {
|
||||
participants: Object.keys(room.participants).length,
|
||||
waypoints: room.waypoints.length,
|
||||
pushSubscriptions: pushSubscriptions.get(slug)?.size || 0,
|
||||
pushSubscriptions: pushSubscriptions.get(slug)?.size ?? 0,
|
||||
lastActivity: room.lastActivity
|
||||
};
|
||||
}
|
||||
|
|
@ -446,7 +451,7 @@ const server = createServer(async (req, res) => {
|
|||
} else if (pathname === '/push/subscribe' && req.method === 'POST') {
|
||||
// Subscribe to push notifications
|
||||
try {
|
||||
const { subscription, roomSlug } = await parseJsonBody(req);
|
||||
const { subscription, roomSlug, participantId } = await parseJsonBody(req);
|
||||
|
||||
if (!subscription || !subscription.endpoint) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
|
|
@ -454,13 +459,16 @@ const server = createServer(async (req, res) => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Store subscription for the room
|
||||
// Store subscription for the room (deduplicates by endpoint automatically via Map key)
|
||||
if (roomSlug) {
|
||||
if (!pushSubscriptions.has(roomSlug)) {
|
||||
pushSubscriptions.set(roomSlug, new Set());
|
||||
pushSubscriptions.set(roomSlug, new Map());
|
||||
}
|
||||
pushSubscriptions.get(roomSlug).add(subscription);
|
||||
console.log(`[${roomSlug}] Push subscription added`);
|
||||
pushSubscriptions.get(roomSlug).set(subscription.endpoint, {
|
||||
subscription,
|
||||
participantId: participantId || null,
|
||||
});
|
||||
console.log(`[${roomSlug}] Push subscription added/updated for participant: ${participantId || 'unknown'}`);
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
|
|
@ -476,13 +484,10 @@ const server = createServer(async (req, res) => {
|
|||
const { endpoint, roomSlug } = await parseJsonBody(req);
|
||||
|
||||
if (roomSlug && pushSubscriptions.has(roomSlug)) {
|
||||
const subs = pushSubscriptions.get(roomSlug);
|
||||
for (const sub of subs) {
|
||||
if (sub.endpoint === endpoint) {
|
||||
subs.delete(sub);
|
||||
console.log(`[${roomSlug}] Push subscription removed`);
|
||||
break;
|
||||
}
|
||||
const subsMap = pushSubscriptions.get(roomSlug);
|
||||
if (subsMap.has(endpoint)) {
|
||||
subsMap.delete(endpoint);
|
||||
console.log(`[${roomSlug}] Push subscription removed`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -563,9 +568,9 @@ const server = createServer(async (req, res) => {
|
|||
}
|
||||
} else if (pathname === '/push/request-location' && req.method === 'POST') {
|
||||
// Manually trigger location request for a room or specific participant
|
||||
// Uses WebSocket for online clients, push for offline/background
|
||||
// Online clients get a silent WebSocket message; offline clients get a VISIBLE push callout
|
||||
try {
|
||||
const { roomSlug, participantId } = await parseJsonBody(req);
|
||||
const { roomSlug, participantId, callerName } = await parseJsonBody(req);
|
||||
|
||||
if (!roomSlug) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
|
|
@ -586,6 +591,14 @@ const server = createServer(async (req, res) => {
|
|||
targetName = room.participants[participantId].name;
|
||||
}
|
||||
|
||||
// Build a set of participantIds that are currently connected via WebSocket
|
||||
const onlineParticipantIds = new Set();
|
||||
for (const [ws, clientInfo] of clients.entries()) {
|
||||
if (clientInfo.roomSlug === roomSlug && ws.readyState === 1 && clientInfo.participantId) {
|
||||
onlineParticipantIds.add(clientInfo.participantId);
|
||||
}
|
||||
}
|
||||
|
||||
// Send WebSocket message to connected clients, deduplicated by name
|
||||
const locationRequestMsg = JSON.stringify({ type: 'request_location' });
|
||||
const pingedNames = new Set();
|
||||
|
|
@ -617,31 +630,74 @@ const server = createServer(async (req, res) => {
|
|||
}
|
||||
console.log(`[${roomSlug}] Location request via WebSocket: ${wsSent} clients${targetName ? ` (target: ${targetName})` : ' (deduped by name)'}`);
|
||||
|
||||
// Then, send push notifications to offline subscribers
|
||||
const subs = pushSubscriptions.get(roomSlug);
|
||||
if (subs && subs.size > 0) {
|
||||
// Send push notifications to subscribers that are NOT online via WebSocket
|
||||
// Online users: silent push (they already got the WS message)
|
||||
// Offline users: visible "callout" notification so they know someone is looking for them
|
||||
const subsMap = pushSubscriptions.get(roomSlug);
|
||||
if (subsMap && subsMap.size > 0) {
|
||||
const failedEndpoints = [];
|
||||
for (const sub of subs) {
|
||||
const pingerName = callerName || 'Someone';
|
||||
|
||||
for (const [endpoint, { subscription, participantId: subParticipantId }] of subsMap.entries()) {
|
||||
// If targeting specific participant, skip others
|
||||
if (targetName && subParticipantId) {
|
||||
const subParticipant = room?.participants?.[subParticipantId];
|
||||
if (subParticipant && subParticipant.name !== targetName) continue;
|
||||
}
|
||||
|
||||
const isOnline = subParticipantId && onlineParticipantIds.has(subParticipantId);
|
||||
|
||||
try {
|
||||
await webpush.sendNotification(sub, JSON.stringify({
|
||||
silent: true,
|
||||
data: { type: 'location_request', roomSlug }
|
||||
}));
|
||||
if (isOnline) {
|
||||
// Online users already got the WS message; send silent push as backup
|
||||
await webpush.sendNotification(subscription, JSON.stringify({
|
||||
silent: true,
|
||||
data: { type: 'location_request', roomSlug }
|
||||
}));
|
||||
} else {
|
||||
// Offline users: send a VISIBLE callout notification
|
||||
await webpush.sendNotification(subscription, JSON.stringify({
|
||||
title: `📍 ${pingerName} is looking for you!`,
|
||||
body: `Tap to share your location in ${roomSlug}`,
|
||||
tag: `callout-${roomSlug}`,
|
||||
data: {
|
||||
type: 'callout',
|
||||
roomSlug,
|
||||
callerName: pingerName,
|
||||
},
|
||||
actions: [
|
||||
{ action: 'view', title: 'Open Map' },
|
||||
],
|
||||
requireInteraction: true,
|
||||
}));
|
||||
}
|
||||
pushSent++;
|
||||
} catch (error) {
|
||||
pushFailed++;
|
||||
if (error.statusCode === 404 || error.statusCode === 410) {
|
||||
failedEndpoints.push(sub.endpoint);
|
||||
failedEndpoints.push(endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up failed subscriptions
|
||||
for (const endpoint of failedEndpoints) {
|
||||
for (const sub of subs) {
|
||||
if (sub.endpoint === endpoint) {
|
||||
subs.delete(sub);
|
||||
}
|
||||
subsMap.delete(endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
// Collect last known locations of targeted participants for the response
|
||||
const lastLocations = {};
|
||||
if (room) {
|
||||
for (const [pid, p] of Object.entries(room.participants)) {
|
||||
if (targetName && p.name !== targetName) continue;
|
||||
if (p.location) {
|
||||
lastLocations[pid] = {
|
||||
name: p.name,
|
||||
location: p.location,
|
||||
status: p.status,
|
||||
lastSeen: p.lastSeen,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -653,7 +709,8 @@ const server = createServer(async (req, res) => {
|
|||
websocket: wsSent,
|
||||
push: pushSent,
|
||||
pushFailed,
|
||||
total: wsSent + pushSent
|
||||
total: wsSent + pushSent,
|
||||
lastLocations,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Location request error:', error);
|
||||
|
|
|
|||
Loading…
Reference in New Issue