PWA auto-opens last room + visible ping notifications

1. Installed PWA (standalone mode) auto-redirects to last visited room
   on launch instead of showing the landing page. Uses localStorage
   rmaps_last_room + display-mode: standalone media query.

2. Manual pings now show visible feedback in three ways:
   - In-app toast: green banner "Alice pinged you for your location!"
   - Browser notification: fires Notification API when permission granted
   - Vibration: unchanged [200, 100, 200, 100, 400] pattern

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-19 00:38:47 +00:00
parent a1132371f4
commit 53dd95fcac
2 changed files with 39 additions and 2 deletions

View File

@ -48,6 +48,7 @@ export default function RoomPage() {
const [zoomToLocation, setZoomToLocation] = useState<{ latitude: number; longitude: number } | null>(null); const [zoomToLocation, setZoomToLocation] = useState<{ latitude: number; longitude: number } | null>(null);
const [needsJoin, setNeedsJoin] = useState(false); const [needsJoin, setNeedsJoin] = useState(false);
const [pendingManualPing, setPendingManualPing] = useState(false); const [pendingManualPing, setPendingManualPing] = useState(false);
const [pingToast, setPingToast] = useState<string | null>(null);
// Check if opened from a manual ping notification (e.g. ?ping=manual&pinger=Alice) // Check if opened from a manual ping notification (e.g. ?ping=manual&pinger=Alice)
const [pingCallerName, setPingCallerName] = useState<string | undefined>(undefined); const [pingCallerName, setPingCallerName] = useState<string | undefined>(undefined);
@ -154,7 +155,27 @@ export default function RoomPage() {
// 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((callerName?: string) => { const handleManualPing = useCallback((callerName?: string) => {
console.log(`Manual ping from ${callerName || 'unknown'} — vibrating and sending GPS`); const pinger = callerName || 'Someone';
console.log(`Manual ping from ${pinger} — vibrating and sending GPS`);
// Show in-app toast
setPingToast(`📍 ${pinger} pinged you for your location!`);
setTimeout(() => setPingToast(null), 4000);
// Show browser notification (works even when tab is backgrounded)
if (typeof Notification !== 'undefined' && Notification.permission === 'granted') {
try {
new Notification(`📍 ${pinger} pinged you!`, {
body: 'Sharing your location now...',
icon: '/icon-192.png',
tag: 'manual-ping',
silent: true, // vibration handles the alert
});
} catch {
// Ignore notification errors (e.g. in standalone PWA)
}
}
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]);
} }
@ -496,6 +517,13 @@ export default function RoomPage() {
</div> </div>
)} )}
{/* Ping toast notification */}
{pingToast && (
<div className="absolute top-4 left-1/2 -translate-x-1/2 bg-emerald-500/90 text-white text-sm px-4 py-2 rounded-full z-30 shadow-lg animate-pulse">
{pingToast}
</div>
)}
{/* Connection status indicator */} {/* Connection status indicator */}
{!isConnected && ( {!isConnected && (
<div className="absolute top-4 left-1/2 -translate-x-1/2 bg-yellow-500/90 text-black text-sm px-3 py-1.5 rounded-full z-30"> <div className="absolute top-4 left-1/2 -translate-x-1/2 bg-yellow-500/90 text-black text-sm px-3 py-1.5 rounded-full z-30">

View File

@ -27,6 +27,7 @@ export default function HomePage() {
const [lastRoom, setLastRoom] = useState<string | null>(null); const [lastRoom, setLastRoom] = useState<string | null>(null);
// Load saved user info from localStorage on mount // Load saved user info from localStorage on mount
// If opened as installed PWA (standalone mode), auto-redirect to last room
useEffect(() => { useEffect(() => {
let loadedEmoji = ''; let loadedEmoji = '';
try { try {
@ -43,6 +44,14 @@ export default function HomePage() {
const lastVisited = localStorage.getItem('rmaps_last_room'); const lastVisited = localStorage.getItem('rmaps_last_room');
if (lastVisited) { if (lastVisited) {
setLastRoom(lastVisited); setLastRoom(lastVisited);
// Auto-redirect if running as installed PWA and user has saved info
const isStandalone = window.matchMedia('(display-mode: standalone)').matches
|| (navigator as unknown as { standalone?: boolean }).standalone === true;
if (isStandalone && stored) {
router.push(`/${lastVisited}`);
return;
}
} }
} catch { } catch {
// Ignore parse errors // Ignore parse errors
@ -52,7 +61,7 @@ export default function HomePage() {
setEmoji(EMOJI_OPTIONS[Math.floor(Math.random() * EMOJI_OPTIONS.length)]); setEmoji(EMOJI_OPTIONS[Math.floor(Math.random() * EMOJI_OPTIONS.length)]);
} }
setIsLoaded(true); setIsLoaded(true);
}, []); }, [router]);
// Auto-fill name from EncryptID when authenticated // Auto-fill name from EncryptID when authenticated
useEffect(() => { useEffect(() => {