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({
|
const clients = await self.clients.matchAll({
|
||||||
type: 'window',
|
type: 'window',
|
||||||
includeUncontrolled: true
|
includeUncontrolled: true
|
||||||
});
|
});
|
||||||
|
|
||||||
if (clients.length > 0) {
|
if (clients.length > 0) {
|
||||||
clients[0].postMessage({ type: 'REQUEST_LOCATION_UPDATE', manual });
|
clients[0].postMessage({ type: 'REQUEST_LOCATION_UPDATE', manual, callerName });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -344,19 +344,20 @@ self.addEventListener('push', (event) => {
|
||||||
lastLocationRequestTime = now;
|
lastLocationRequestTime = now;
|
||||||
|
|
||||||
const isManual = !!(data.data?.manual);
|
const isManual = !!(data.data?.manual);
|
||||||
|
const callerName = data.data?.callerName || 'Someone';
|
||||||
|
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
requestLocationFromClient(isManual).then(async (sent) => {
|
requestLocationFromClient(isManual, callerName).then(async (sent) => {
|
||||||
console.log('[SW] Push location request sent:', sent, 'manual:', isManual);
|
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
|
// 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
|
// so the user can tap to open the app and auto-respond with location
|
||||||
if (!sent && isManual) {
|
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',
|
body: 'Tap to share your location',
|
||||||
icon: '/icon-192.png',
|
icon: '/icon-192.png',
|
||||||
badge: '/icon-192.png',
|
badge: '/icon-192.png',
|
||||||
tag: `callout-${data.data?.roomSlug || 'unknown'}`,
|
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],
|
vibrate: [200, 100, 200, 100, 400],
|
||||||
requireInteraction: true,
|
requireInteraction: true,
|
||||||
actions: [{ action: 'view', title: 'Open Map' }],
|
actions: [{ action: 'view', title: 'Open Map' }],
|
||||||
|
|
@ -434,9 +435,12 @@ self.addEventListener('notificationclick', (event) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (clients.openWindow) {
|
if (clients.openWindow) {
|
||||||
// If manual ping, append ?ping=manual so the app auto-responds with GPS on load
|
// If manual ping, append ?ping=manual&pinger=<name> so the app auto-responds with GPS on load
|
||||||
const openUrl = data.manual ? `${targetUrl}?ping=manual` : targetUrl;
|
if (data.manual) {
|
||||||
return clients.openWindow(openUrl);
|
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 [needsJoin, setNeedsJoin] = useState(false);
|
||||||
const [pendingManualPing, setPendingManualPing] = 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(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
if (params.get('ping') === 'manual') {
|
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('ping');
|
||||||
|
params.delete('pinger');
|
||||||
const newUrl = params.toString()
|
const newUrl = params.toString()
|
||||||
? `${window.location.pathname}?${params.toString()}`
|
? `${window.location.pathname}?${params.toString()}`
|
||||||
: window.location.pathname;
|
: window.location.pathname;
|
||||||
window.history.replaceState({}, '', newUrl);
|
window.history.replaceState({}, '', newUrl);
|
||||||
|
setPingCallerName(pinger);
|
||||||
setPendingManualPing(true);
|
setPendingManualPing(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -149,7 +153,8 @@ export default function RoomPage() {
|
||||||
}, [slug]);
|
}, [slug]);
|
||||||
|
|
||||||
// Handle manual ping: vibrate device + one-shot GPS regardless of sharing state
|
// 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) {
|
if (typeof navigator !== 'undefined' && 'vibrate' in navigator) {
|
||||||
navigator.vibrate([200, 100, 200, 100, 400]);
|
navigator.vibrate([200, 100, 200, 100, 400]);
|
||||||
}
|
}
|
||||||
|
|
@ -188,10 +193,10 @@ export default function RoomPage() {
|
||||||
|
|
||||||
// Service worker messages for background location sync
|
// Service worker messages for background location sync
|
||||||
useServiceWorkerMessages({
|
useServiceWorkerMessages({
|
||||||
onLocationRequest: (manual?: boolean) => {
|
onLocationRequest: (manual?: boolean, callerName?: string) => {
|
||||||
console.log('Service worker requested location update, manual:', manual);
|
console.log('Service worker requested location update, manual:', manual, 'from:', callerName);
|
||||||
if (manual) {
|
if (manual) {
|
||||||
handleManualPing();
|
handleManualPing(callerName);
|
||||||
} else if (isSharing) {
|
} else if (isSharing) {
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
@ -227,11 +232,11 @@ export default function RoomPage() {
|
||||||
// Execute pending manual ping once connected (from notification tap while app was closed)
|
// Execute pending manual ping once connected (from notification tap while app was closed)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pendingManualPing && isConnected) {
|
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);
|
setPendingManualPing(false);
|
||||||
handleManualPing();
|
handleManualPing(pingCallerName);
|
||||||
}
|
}
|
||||||
}, [pendingManualPing, isConnected, handleManualPing]);
|
}, [pendingManualPing, isConnected, handleManualPing, pingCallerName]);
|
||||||
|
|
||||||
// Restore last known location immediately when connected
|
// Restore last known location immediately when connected
|
||||||
const hasRestoredLocationRef = useRef(false);
|
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)
|
// Set up callback for when server requests location (via ping button or auto)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
setLocationRequestCallback((manual?: boolean) => {
|
setLocationRequestCallback((manual?: boolean, callerName?: string) => {
|
||||||
console.log('Server requested location update, manual:', manual);
|
console.log('Server requested location update, manual:', manual, 'from:', callerName);
|
||||||
if (manual) {
|
if (manual) {
|
||||||
handleManualPing();
|
handleManualPing(callerName);
|
||||||
} else if (isSharing) {
|
} else if (isSharing) {
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ interface UseRoomReturn {
|
||||||
addWaypoint: (waypoint: Omit<Waypoint, 'id' | 'createdAt' | 'createdBy'>) => void;
|
addWaypoint: (waypoint: Omit<Waypoint, 'id' | 'createdAt' | 'createdBy'>) => void;
|
||||||
removeWaypoint: (waypointId: string) => void;
|
removeWaypoint: (waypointId: string) => void;
|
||||||
leave: () => 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 {
|
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)
|
// 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) {
|
if (syncRef.current) {
|
||||||
syncRef.current.setLocationRequestCallback(callback);
|
syncRef.current.setLocationRequestCallback(callback);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ interface ServiceWorkerRegistrationWithSync extends ServiceWorkerRegistration {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseServiceWorkerMessagesOptions {
|
interface UseServiceWorkerMessagesOptions {
|
||||||
onLocationRequest?: (manual?: boolean) => void;
|
onLocationRequest?: (manual?: boolean, callerName?: string) => void;
|
||||||
onLocationSync?: () => void;
|
onLocationSync?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,7 +35,7 @@ export function useServiceWorkerMessages(options: UseServiceWorkerMessagesOption
|
||||||
switch (event.data?.type) {
|
switch (event.data?.type) {
|
||||||
case 'REQUEST_LOCATION_UPDATE':
|
case 'REQUEST_LOCATION_UPDATE':
|
||||||
// Service worker is requesting a location update (from push)
|
// 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;
|
break;
|
||||||
|
|
||||||
case 'REQUEST_LOCATION_SYNC':
|
case 'REQUEST_LOCATION_SYNC':
|
||||||
|
|
|
||||||
|
|
@ -77,11 +77,11 @@ export type SyncMessage =
|
||||||
| { type: 'waypoint_remove'; waypointId: string }
|
| { type: 'waypoint_remove'; waypointId: string }
|
||||||
| { type: 'full_state'; state: RoomState }
|
| { type: 'full_state'; state: RoomState }
|
||||||
| { type: 'request_state' }
|
| { type: 'request_state' }
|
||||||
| { type: 'request_location'; manual?: boolean };
|
| { type: 'request_location'; manual?: boolean; callerName?: string };
|
||||||
|
|
||||||
type SyncCallback = (state: RoomState) => void;
|
type SyncCallback = (state: RoomState) => void;
|
||||||
type ConnectionCallback = (connected: boolean) => 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)
|
// Validate that coordinates are reasonable (not 0,0 or out of bounds)
|
||||||
function isValidLocation(location: LocationState | undefined): boolean {
|
function isValidLocation(location: LocationState | undefined): boolean {
|
||||||
|
|
@ -328,9 +328,9 @@ export class RoomSync {
|
||||||
|
|
||||||
case 'request_location':
|
case 'request_location':
|
||||||
// Server is requesting a location update from us
|
// 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) {
|
if (this.onLocationRequest) {
|
||||||
this.onLocationRequest(message.manual);
|
this.onLocationRequest(message.manual, message.callerName);
|
||||||
}
|
}
|
||||||
return; // Don't notify state change for this message type
|
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
|
// 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();
|
const pingedNames = new Set();
|
||||||
|
|
||||||
for (const [ws, clientInfo] of clients.entries()) {
|
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
|
// Online users already got the WS message; send silent push as backup
|
||||||
await webpush.sendNotification(subscription, JSON.stringify({
|
await webpush.sendNotification(subscription, JSON.stringify({
|
||||||
silent: true,
|
silent: true,
|
||||||
data: { type: 'location_request', roomSlug, manual: true }
|
data: { type: 'location_request', roomSlug, manual: true, callerName: pingerName }
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
// Offline users: send a VISIBLE callout notification
|
// Offline users: send a VISIBLE callout notification
|
||||||
await webpush.sendNotification(subscription, JSON.stringify({
|
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}`,
|
body: `Tap to share your location in ${roomSlug}`,
|
||||||
tag: `callout-${roomSlug}`,
|
tag: `callout-${roomSlug}`,
|
||||||
data: {
|
data: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue