feat: Add refresh locations button to participant list

- Sends silent push to all friends requesting location updates
- Shows spinner while refreshing
- Displays feedback message with count of pinged friends

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2025-12-29 01:35:32 +01:00
parent 4ee33f4992
commit 4ab4f28e70
2 changed files with 86 additions and 13 deletions

View File

@ -308,6 +308,8 @@ export default function RoomPage() {
<ParticipantList
participants={participants}
currentUserId={currentUser.name}
roomSlug={slug}
syncUrl={process.env.NEXT_PUBLIC_SYNC_URL}
onClose={() => setShowParticipants(false)}
onNavigateTo={handleNavigateTo}
onSetMeetingPoint={() => setShowMeetingPoint(true)}

View File

@ -1,10 +1,13 @@
'use client';
import { useState, useCallback } from 'react';
import type { Participant } from '@/types';
interface ParticipantListProps {
participants: Participant[];
currentUserId?: string;
roomSlug: string;
syncUrl?: string;
onClose: () => void;
onNavigateTo: (participant: Participant) => void;
onSetMeetingPoint?: () => void;
@ -13,10 +16,46 @@ interface ParticipantListProps {
export default function ParticipantList({
participants,
currentUserId,
roomSlug,
syncUrl,
onClose,
onNavigateTo,
onSetMeetingPoint,
}: ParticipantListProps) {
const [isRefreshing, setIsRefreshing] = useState(false);
const [refreshMessage, setRefreshMessage] = useState<string | null>(null);
const handleRefreshLocations = useCallback(async () => {
if (!syncUrl || isRefreshing) return;
setIsRefreshing(true);
setRefreshMessage(null);
try {
const httpUrl = syncUrl.replace('wss://', 'https://').replace('ws://', 'http://');
const response = await fetch(`${httpUrl}/push/request-location`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ roomSlug }),
});
const data = await response.json();
if (data.success) {
if (data.sent > 0) {
setRefreshMessage(`Pinged ${data.sent} friend${data.sent > 1 ? 's' : ''}`);
} else {
setRefreshMessage('No friends to ping');
}
}
} catch (error) {
console.error('Failed to refresh locations:', error);
setRefreshMessage('Failed to ping');
} finally {
setIsRefreshing(false);
// Clear message after 3 seconds
setTimeout(() => setRefreshMessage(null), 3000);
}
}, [syncUrl, roomSlug, isRefreshing]);
const formatDistance = (participant: Participant, current: Participant | undefined) => {
if (!participant.location || !current?.location) return null;
@ -50,6 +89,30 @@ export default function ParticipantList({
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-white/10">
<h2 className="font-semibold">Friends ({participants.length})</h2>
<div className="flex items-center gap-2">
{/* Refresh locations button */}
{syncUrl && (
<button
onClick={handleRefreshLocations}
disabled={isRefreshing}
className="p-1.5 rounded hover:bg-white/10 transition-colors disabled:opacity-50"
title="Ping friends for location updates"
>
<svg
className={`w-5 h-5 ${isRefreshing ? 'animate-spin' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
</button>
)}
<button
onClick={onClose}
className="p-1 rounded hover:bg-white/10 transition-colors"
@ -64,6 +127,14 @@ export default function ParticipantList({
</svg>
</button>
</div>
</div>
{/* Refresh status message */}
{refreshMessage && (
<div className="px-4 py-2 bg-rmaps-primary/20 text-rmaps-primary text-sm text-center">
{refreshMessage}
</div>
)}
{/* Participant list */}
<div className="flex-1 overflow-y-auto p-2">