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:
parent
c3f884d2c4
commit
a1132371f4
20
public/sw.js
20
public/sw.js
|
|
@ -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);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue