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:
parent
4ee33f4992
commit
4ab4f28e70
|
|
@ -308,6 +308,8 @@ export default function RoomPage() {
|
||||||
<ParticipantList
|
<ParticipantList
|
||||||
participants={participants}
|
participants={participants}
|
||||||
currentUserId={currentUser.name}
|
currentUserId={currentUser.name}
|
||||||
|
roomSlug={slug}
|
||||||
|
syncUrl={process.env.NEXT_PUBLIC_SYNC_URL}
|
||||||
onClose={() => setShowParticipants(false)}
|
onClose={() => setShowParticipants(false)}
|
||||||
onNavigateTo={handleNavigateTo}
|
onNavigateTo={handleNavigateTo}
|
||||||
onSetMeetingPoint={() => setShowMeetingPoint(true)}
|
onSetMeetingPoint={() => setShowMeetingPoint(true)}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
import type { Participant } from '@/types';
|
import type { Participant } from '@/types';
|
||||||
|
|
||||||
interface ParticipantListProps {
|
interface ParticipantListProps {
|
||||||
participants: Participant[];
|
participants: Participant[];
|
||||||
currentUserId?: string;
|
currentUserId?: string;
|
||||||
|
roomSlug: string;
|
||||||
|
syncUrl?: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onNavigateTo: (participant: Participant) => void;
|
onNavigateTo: (participant: Participant) => void;
|
||||||
onSetMeetingPoint?: () => void;
|
onSetMeetingPoint?: () => void;
|
||||||
|
|
@ -13,10 +16,46 @@ interface ParticipantListProps {
|
||||||
export default function ParticipantList({
|
export default function ParticipantList({
|
||||||
participants,
|
participants,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
|
roomSlug,
|
||||||
|
syncUrl,
|
||||||
onClose,
|
onClose,
|
||||||
onNavigateTo,
|
onNavigateTo,
|
||||||
onSetMeetingPoint,
|
onSetMeetingPoint,
|
||||||
}: ParticipantListProps) {
|
}: 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) => {
|
const formatDistance = (participant: Participant, current: Participant | undefined) => {
|
||||||
if (!participant.location || !current?.location) return null;
|
if (!participant.location || !current?.location) return null;
|
||||||
|
|
||||||
|
|
@ -50,6 +89,30 @@ export default function ParticipantList({
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between p-4 border-b border-white/10">
|
<div className="flex items-center justify-between p-4 border-b border-white/10">
|
||||||
<h2 className="font-semibold">Friends ({participants.length})</h2>
|
<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
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="p-1 rounded hover:bg-white/10 transition-colors"
|
className="p-1 rounded hover:bg-white/10 transition-colors"
|
||||||
|
|
@ -64,6 +127,14 @@ export default function ParticipantList({
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 */}
|
{/* Participant list */}
|
||||||
<div className="flex-1 overflow-y-auto p-2">
|
<div className="flex-1 overflow-y-auto p-2">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue