Thread caller name through ping notifications

Ping notifications now show who pinged: "<Name> pinged you for your
location!" instead of generic "Someone is looking for you". Caller name
flows through WS messages, silent push payloads, SW postMessage, URL
params (?pinger=), and visible push notifications.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-19 00:35:54 +00:00
parent c3f884d2c4
commit a1132371f4
6 changed files with 40 additions and 31 deletions

View File

@ -293,14 +293,14 @@ async function syncLocation() {
}
}
async function requestLocationFromClient(manual = false) {
async function requestLocationFromClient(manual = false, callerName = null) {
const clients = await self.clients.matchAll({
type: 'window',
includeUncontrolled: true
});
if (clients.length > 0) {
clients[0].postMessage({ type: 'REQUEST_LOCATION_UPDATE', manual });
clients[0].postMessage({ type: 'REQUEST_LOCATION_UPDATE', manual, callerName });
return true;
}
return false;
@ -344,19 +344,20 @@ self.addEventListener('push', (event) => {
lastLocationRequestTime = now;
const isManual = !!(data.data?.manual);
const callerName = data.data?.callerName || 'Someone';
event.waitUntil(
requestLocationFromClient(isManual).then(async (sent) => {
requestLocationFromClient(isManual, callerName).then(async (sent) => {
console.log('[SW] Push location request sent:', sent, 'manual:', isManual);
// If no app window is open and this is a manual ping, show a visible notification
// so the user can tap to open the app and auto-respond with location
if (!sent && isManual) {
await self.registration.showNotification('📍 Someone is looking for you!', {
await self.registration.showNotification(`📍 ${callerName} pinged you for your location!`, {
body: 'Tap to share your location',
icon: '/icon-192.png',
badge: '/icon-192.png',
tag: `callout-${data.data?.roomSlug || 'unknown'}`,
data: { type: 'callout', roomSlug: data.data?.roomSlug, manual: true },
data: { type: 'callout', roomSlug: data.data?.roomSlug, manual: true, callerName },
vibrate: [200, 100, 200, 100, 400],
requireInteraction: true,
actions: [{ action: 'view', title: 'Open Map' }],
@ -434,9 +435,12 @@ self.addEventListener('notificationclick', (event) => {
}
}
if (clients.openWindow) {
// If manual ping, append ?ping=manual so the app auto-responds with GPS on load
const openUrl = data.manual ? `${targetUrl}?ping=manual` : targetUrl;
return clients.openWindow(openUrl);
// If manual ping, append ?ping=manual&pinger=<name> so the app auto-responds with GPS on load
if (data.manual) {
const pingerParam = data.callerName ? `&pinger=${encodeURIComponent(data.callerName)}` : '';
return clients.openWindow(`${targetUrl}?ping=manual${pingerParam}`);
}
return clients.openWindow(targetUrl);
}
})
);

View File

@ -49,17 +49,21 @@ export default function RoomPage() {
const [needsJoin, setNeedsJoin] = useState(false);
const [pendingManualPing, setPendingManualPing] = useState(false);
// Check if opened from a manual ping notification (e.g. ?ping=manual)
// Check if opened from a manual ping notification (e.g. ?ping=manual&pinger=Alice)
const [pingCallerName, setPingCallerName] = useState<string | undefined>(undefined);
useEffect(() => {
if (typeof window !== 'undefined') {
const params = new URLSearchParams(window.location.search);
if (params.get('ping') === 'manual') {
// Clean the param from URL
const pinger = params.get('pinger') || undefined;
// Clean the params from URL
params.delete('ping');
params.delete('pinger');
const newUrl = params.toString()
? `${window.location.pathname}?${params.toString()}`
: window.location.pathname;
window.history.replaceState({}, '', newUrl);
setPingCallerName(pinger);
setPendingManualPing(true);
}
}
@ -149,7 +153,8 @@ export default function RoomPage() {
}, [slug]);
// Handle manual ping: vibrate device + one-shot GPS regardless of sharing state
const handleManualPing = useCallback(() => {
const handleManualPing = useCallback((callerName?: string) => {
console.log(`Manual ping from ${callerName || 'unknown'} — vibrating and sending GPS`);
if (typeof navigator !== 'undefined' && 'vibrate' in navigator) {
navigator.vibrate([200, 100, 200, 100, 400]);
}
@ -188,10 +193,10 @@ export default function RoomPage() {
// Service worker messages for background location sync
useServiceWorkerMessages({
onLocationRequest: (manual?: boolean) => {
console.log('Service worker requested location update, manual:', manual);
onLocationRequest: (manual?: boolean, callerName?: string) => {
console.log('Service worker requested location update, manual:', manual, 'from:', callerName);
if (manual) {
handleManualPing();
handleManualPing(callerName);
} else if (isSharing) {
requestUpdate();
}
@ -227,11 +232,11 @@ export default function RoomPage() {
// Execute pending manual ping once connected (from notification tap while app was closed)
useEffect(() => {
if (pendingManualPing && isConnected) {
console.log('Executing pending manual ping from notification tap');
console.log('Executing pending manual ping from notification tap, pinger:', pingCallerName);
setPendingManualPing(false);
handleManualPing();
handleManualPing(pingCallerName);
}
}, [pendingManualPing, isConnected, handleManualPing]);
}, [pendingManualPing, isConnected, handleManualPing, pingCallerName]);
// Restore last known location immediately when connected
const hasRestoredLocationRef = useRef(false);
@ -273,10 +278,10 @@ export default function RoomPage() {
// Set up callback for when server requests location (via ping button or auto)
useEffect(() => {
if (isConnected) {
setLocationRequestCallback((manual?: boolean) => {
console.log('Server requested location update, manual:', manual);
setLocationRequestCallback((manual?: boolean, callerName?: string) => {
console.log('Server requested location update, manual:', manual, 'from:', callerName);
if (manual) {
handleManualPing();
handleManualPing(callerName);
} else if (isSharing) {
requestUpdate();
}

View File

@ -118,7 +118,7 @@ interface UseRoomReturn {
addWaypoint: (waypoint: Omit<Waypoint, 'id' | 'createdAt' | 'createdBy'>) => void;
removeWaypoint: (waypointId: string) => void;
leave: () => void;
setLocationRequestCallback: (callback: () => void) => void;
setLocationRequestCallback: (callback: (manual?: boolean, callerName?: string) => void) => void;
}
export function useRoom({ slug, userName, userEmoji, encryptIdDid, authToken }: UseRoomOptions): UseRoomReturn {
@ -296,7 +296,7 @@ export function useRoom({ slug, userName, userEmoji, encryptIdDid, authToken }:
}, []);
// Set location request callback (called when server requests location update)
const setLocationRequestCallback = useCallback((callback: () => void) => {
const setLocationRequestCallback = useCallback((callback: (manual?: boolean, callerName?: string) => void) => {
if (syncRef.current) {
syncRef.current.setLocationRequestCallback(callback);
}

View File

@ -12,7 +12,7 @@ interface ServiceWorkerRegistrationWithSync extends ServiceWorkerRegistration {
}
interface UseServiceWorkerMessagesOptions {
onLocationRequest?: (manual?: boolean) => void;
onLocationRequest?: (manual?: boolean, callerName?: string) => void;
onLocationSync?: () => void;
}
@ -35,7 +35,7 @@ export function useServiceWorkerMessages(options: UseServiceWorkerMessagesOption
switch (event.data?.type) {
case 'REQUEST_LOCATION_UPDATE':
// Service worker is requesting a location update (from push)
optionsRef.current.onLocationRequest?.(event.data?.manual);
optionsRef.current.onLocationRequest?.(event.data?.manual, event.data?.callerName);
break;
case 'REQUEST_LOCATION_SYNC':

View File

@ -77,11 +77,11 @@ export type SyncMessage =
| { type: 'waypoint_remove'; waypointId: string }
| { type: 'full_state'; state: RoomState }
| { type: 'request_state' }
| { type: 'request_location'; manual?: boolean };
| { type: 'request_location'; manual?: boolean; callerName?: string };
type SyncCallback = (state: RoomState) => void;
type ConnectionCallback = (connected: boolean) => void;
type LocationRequestCallback = (manual?: boolean) => void;
type LocationRequestCallback = (manual?: boolean, callerName?: string) => void;
// Validate that coordinates are reasonable (not 0,0 or out of bounds)
function isValidLocation(location: LocationState | undefined): boolean {
@ -328,9 +328,9 @@ export class RoomSync {
case 'request_location':
// Server is requesting a location update from us
console.log('[RoomSync] Received location request, manual:', message.manual);
console.log('[RoomSync] Received location request, manual:', message.manual, 'from:', message.callerName);
if (this.onLocationRequest) {
this.onLocationRequest(message.manual);
this.onLocationRequest(message.manual, message.callerName);
}
return; // Don't notify state change for this message type
}

View File

@ -718,7 +718,7 @@ const server = createServer(async (req, res) => {
}
// Send WebSocket message to connected clients, deduplicated by name
const locationRequestMsg = JSON.stringify({ type: 'request_location', manual: true });
const locationRequestMsg = JSON.stringify({ type: 'request_location', manual: true, callerName: pingerName });
const pingedNames = new Set();
for (const [ws, clientInfo] of clients.entries()) {
@ -770,12 +770,12 @@ const server = createServer(async (req, res) => {
// 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, manual: true }
data: { type: 'location_request', roomSlug, manual: true, callerName: pingerName }
}));
} else {
// Offline users: send a VISIBLE callout notification
await webpush.sendNotification(subscription, JSON.stringify({
title: `📍 ${pingerName} is looking for you!`,
title: `📍 ${pingerName} pinged you for your location!`,
body: `Tap to share your location in ${roomSlug}`,
tag: `callout-${roomSlug}`,
data: {