feat: Add WebSocket-based location refresh for online friends
- Server sends request_location via WebSocket to connected clients - Falls back to silent push for offline/background clients - Client responds with current GPS location when requested - Refresh button now works for online friends (no push subscription needed) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
aa501b1778
commit
d6a20f9500
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ interface UseRoomReturn {
|
|||
addWaypoint: (waypoint: Omit<Waypoint, 'id' | 'createdAt' | 'createdBy'>) => 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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<typeof setTimeout> | 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();
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
|
|
|
|||
Loading…
Reference in New Issue