diff --git a/src/app/[slug]/page.tsx b/src/app/[slug]/page.tsx index 01f5eb0..4f7a822 100644 --- a/src/app/[slug]/page.tsx +++ b/src/app/[slug]/page.tsx @@ -70,6 +70,7 @@ export default function RoomPage() { addWaypoint, removeWaypoint, leave, + setLocationRequestCallback, } = useRoom({ slug, userName: currentUser?.name || '', @@ -175,6 +176,18 @@ export default function RoomPage() { } }, [shouldAutoStartSharing, isConnected, isSharing, startSharing]); + // Set up callback for when server requests location (via refresh button) + useEffect(() => { + if (isConnected) { + setLocationRequestCallback(() => { + console.log('Server requested location update'); + if (isSharing) { + requestUpdate(); + } + }); + } + }, [isConnected, isSharing, requestUpdate, setLocationRequestCallback]); + // Handler for toggling location sharing - persists preference const handleToggleSharing = useCallback(() => { if (isSharing) { diff --git a/src/components/room/ParticipantList.tsx b/src/components/room/ParticipantList.tsx index b186fbd..6dd3f79 100644 --- a/src/components/room/ParticipantList.tsx +++ b/src/components/room/ParticipantList.tsx @@ -41,8 +41,9 @@ export default function ParticipantList({ const data = await response.json(); if (data.success) { - if (data.sent > 0) { - setRefreshMessage(`Pinged ${data.sent} friend${data.sent > 1 ? 's' : ''}`); + const total = data.total || (data.websocket || 0) + (data.push || 0); + if (total > 0) { + setRefreshMessage(`Pinged ${total} friend${total > 1 ? 's' : ''}`); } else { setRefreshMessage('No friends to ping'); } diff --git a/src/hooks/useRoom.ts b/src/hooks/useRoom.ts index ed6a3fb..758e43b 100644 --- a/src/hooks/useRoom.ts +++ b/src/hooks/useRoom.ts @@ -74,6 +74,7 @@ interface UseRoomReturn { addWaypoint: (waypoint: Omit) => void; removeWaypoint: (waypointId: string) => void; leave: () => void; + setLocationRequestCallback: (callback: () => void) => void; } export function useRoom({ slug, userName, userEmoji }: UseRoomOptions): UseRoomReturn { @@ -225,6 +226,13 @@ export function useRoom({ slug, userName, userEmoji }: UseRoomOptions): UseRoomR setIsConnected(false); }, []); + // Set location request callback (called when server requests location update) + const setLocationRequestCallback = useCallback((callback: () => void) => { + if (syncRef.current) { + syncRef.current.setLocationRequestCallback(callback); + } + }, []); + return { isConnected, isLoading, @@ -240,5 +248,6 @@ export function useRoom({ slug, userName, userEmoji }: UseRoomOptions): UseRoomR addWaypoint, removeWaypoint, leave, + setLocationRequestCallback, }; } diff --git a/src/lib/sync.ts b/src/lib/sync.ts index 735e457..9993418 100644 --- a/src/lib/sync.ts +++ b/src/lib/sync.ts @@ -76,10 +76,12 @@ export type SyncMessage = | { type: 'waypoint_add'; waypoint: WaypointState } | { type: 'waypoint_remove'; waypointId: string } | { type: 'full_state'; state: RoomState } - | { type: 'request_state' }; + | { type: 'request_state' } + | { type: 'request_location' }; type SyncCallback = (state: RoomState) => void; type ConnectionCallback = (connected: boolean) => void; +type LocationRequestCallback = () => void; // Validate that coordinates are reasonable (not 0,0 or out of bounds) function isValidLocation(location: LocationState | undefined): boolean { @@ -101,6 +103,7 @@ export class RoomSync { private reconnectTimer: ReturnType | null = null; private onStateChange: SyncCallback; private onConnectionChange: ConnectionCallback; + private onLocationRequest: LocationRequestCallback | null = null; private participantId: string; private currentParticipant: ParticipantState | null = null; @@ -108,17 +111,23 @@ export class RoomSync { slug: string, participantId: string, onStateChange: SyncCallback, - onConnectionChange: ConnectionCallback + onConnectionChange: ConnectionCallback, + onLocationRequest?: LocationRequestCallback ) { this.slug = slug; this.participantId = participantId; this.onStateChange = onStateChange; this.onConnectionChange = onConnectionChange; + this.onLocationRequest = onLocationRequest || null; // Initialize or load state this.state = this.loadState() || this.createInitialState(); } + setLocationRequestCallback(callback: LocationRequestCallback): void { + this.onLocationRequest = callback; + } + private createInitialState(): RoomState { return { id: crypto.randomUUID(), @@ -310,6 +319,14 @@ export class RoomSync { (w) => w.id !== message.waypointId ); break; + + case 'request_location': + // Server is requesting a location update from us + console.log('[RoomSync] Received location request from server'); + if (this.onLocationRequest) { + this.onLocationRequest(); + } + return; // Don't notify state change for this message type } this.notifyStateChange(); diff --git a/sync-server/server.js b/sync-server/server.js index fbbcacf..3112be2 100644 --- a/sync-server/server.js +++ b/sync-server/server.js @@ -471,6 +471,7 @@ const server = createServer(async (req, res) => { } } else if (pathname === '/push/request-location' && req.method === 'POST') { // Manually trigger location request for a room + // Uses WebSocket for online clients, push for offline/background try { const { roomSlug } = await parseJsonBody(req); @@ -480,44 +481,58 @@ const server = createServer(async (req, res) => { return; } + let wsSent = 0; + let pushSent = 0; + let pushFailed = 0; + + // First, send WebSocket message to all connected clients in the room + const locationRequestMsg = JSON.stringify({ type: 'request_location' }); + for (const [ws, clientInfo] of clients.entries()) { + if (clientInfo.roomSlug === roomSlug && ws.readyState === 1) { + ws.send(locationRequestMsg); + wsSent++; + } + } + console.log(`[${roomSlug}] Location request via WebSocket: ${wsSent} clients`); + + // Then, send push notifications to offline subscribers const subs = pushSubscriptions.get(roomSlug); - if (!subs || subs.size === 0) { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ success: true, sent: 0, message: 'No subscribers in room' })); - return; - } - - let sent = 0; - let failed = 0; - const failedEndpoints = []; - - for (const sub of subs) { - try { - await webpush.sendNotification(sub, JSON.stringify({ - silent: true, - data: { type: 'location_request', roomSlug } - })); - sent++; - } catch (error) { - failed++; - if (error.statusCode === 404 || error.statusCode === 410) { - failedEndpoints.push(sub.endpoint); - } - } - } - - // Clean up failed subscriptions - for (const endpoint of failedEndpoints) { + if (subs && subs.size > 0) { + const failedEndpoints = []; for (const sub of subs) { - if (sub.endpoint === endpoint) { - subs.delete(sub); + try { + await webpush.sendNotification(sub, JSON.stringify({ + silent: true, + data: { type: 'location_request', roomSlug } + })); + pushSent++; + } catch (error) { + pushFailed++; + if (error.statusCode === 404 || error.statusCode === 410) { + failedEndpoints.push(sub.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}] Manual location request: ${sent} sent, ${failed} failed`); + console.log(`[${roomSlug}] Manual location request: ${wsSent} WebSocket, ${pushSent} push sent, ${pushFailed} push failed`); res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ success: true, sent, failed })); + res.end(JSON.stringify({ + success: true, + websocket: wsSent, + push: pushSent, + pushFailed, + total: wsSent + pushSent + })); } catch (error) { console.error('Location request error:', error); res.writeHead(500, { 'Content-Type': 'application/json' });