fix: Deduplicate participants by name across map and list
Added useMemo-based deduplication that keeps the most recently seen participant for each name, preventing duplicate markers on the map and duplicate entries in the friends list. 🤖 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
796fd2c727
commit
47dea7c9c3
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState, useMemo } from 'react';
|
||||
import maplibregl from 'maplibre-gl';
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
import type { Participant, MapViewport, Waypoint, RouteSegment } from '@/types';
|
||||
|
|
@ -83,6 +83,18 @@ export default function MapView({
|
|||
const [mapLoaded, setMapLoaded] = useState(false);
|
||||
const hasCenteredOnUserRef = useRef(false);
|
||||
|
||||
// Deduplicate participants by name, keeping the most recently seen one
|
||||
const dedupedParticipants = useMemo(() => {
|
||||
const byName = new Map<string, Participant>();
|
||||
for (const p of participants) {
|
||||
const existing = byName.get(p.name);
|
||||
if (!existing || p.lastSeen > existing.lastSeen) {
|
||||
byName.set(p.name, p);
|
||||
}
|
||||
}
|
||||
return Array.from(byName.values());
|
||||
}, [participants]);
|
||||
|
||||
// Initialize map
|
||||
useEffect(() => {
|
||||
if (!mapContainer.current || map.current) return;
|
||||
|
|
@ -160,13 +172,13 @@ export default function MapView({
|
|||
useEffect(() => {
|
||||
if (!map.current || !mapLoaded) return;
|
||||
|
||||
console.log('MapView: Updating markers for', participants.length, 'participants');
|
||||
participants.forEach((p) => {
|
||||
console.log('MapView: Updating markers for', dedupedParticipants.length, 'participants');
|
||||
dedupedParticipants.forEach((p) => {
|
||||
console.log(' -', p.name, p.id, 'location:', p.location ? `${p.location.latitude}, ${p.location.longitude}` : 'none');
|
||||
});
|
||||
|
||||
const currentMarkers = markersRef.current;
|
||||
const participantIds = new Set(participants.map((p) => p.id));
|
||||
const participantIds = new Set(dedupedParticipants.map((p) => p.id));
|
||||
|
||||
// Remove markers for participants who left
|
||||
currentMarkers.forEach((marker, id) => {
|
||||
|
|
@ -177,7 +189,7 @@ export default function MapView({
|
|||
});
|
||||
|
||||
// Add/update markers for current participants
|
||||
participants.forEach((participant) => {
|
||||
dedupedParticipants.forEach((participant) => {
|
||||
if (!participant.location) return;
|
||||
|
||||
const { latitude, longitude } = participant.location;
|
||||
|
|
@ -221,7 +233,7 @@ export default function MapView({
|
|||
|
||||
// Auto-center on current user's first location
|
||||
if (autoCenterOnUser && !hasCenteredOnUserRef.current && currentUserId) {
|
||||
const currentUser = participants.find(p => p.id === currentUserId);
|
||||
const currentUser = dedupedParticipants.find(p => p.id === currentUserId);
|
||||
if (currentUser?.location && map.current) {
|
||||
const { latitude, longitude } = currentUser.location;
|
||||
if (isValidCoordinate(latitude, longitude)) {
|
||||
|
|
@ -234,7 +246,7 @@ export default function MapView({
|
|||
}
|
||||
}
|
||||
}
|
||||
}, [participants, mapLoaded, currentUserId, onParticipantClick, autoCenterOnUser]);
|
||||
}, [dedupedParticipants, mapLoaded, currentUserId, onParticipantClick, autoCenterOnUser]);
|
||||
|
||||
// Fly to location when zoomToLocation changes
|
||||
useEffect(() => {
|
||||
|
|
@ -348,9 +360,9 @@ export default function MapView({
|
|||
|
||||
// Fit bounds to show all participants
|
||||
const fitToParticipants = () => {
|
||||
if (!map.current || participants.length === 0) return;
|
||||
if (!map.current || dedupedParticipants.length === 0) return;
|
||||
|
||||
const locatedParticipants = participants.filter(
|
||||
const locatedParticipants = dedupedParticipants.filter(
|
||||
(p) => p.location && isValidCoordinate(p.location.latitude, p.location.longitude)
|
||||
);
|
||||
if (locatedParticipants.length === 0) return;
|
||||
|
|
@ -393,7 +405,7 @@ export default function MapView({
|
|||
)}
|
||||
|
||||
{/* Fit all button */}
|
||||
{participants.some((p) => p.location) && (
|
||||
{dedupedParticipants.some((p) => p.location) && (
|
||||
<button
|
||||
onClick={fitToParticipants}
|
||||
className="absolute bottom-4 right-4 bg-rmaps-dark/90 text-white px-3 py-2 rounded-lg text-sm hover:bg-rmaps-dark transition-colors"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import type { Participant } from '@/types';
|
||||
|
||||
interface ParticipantListProps {
|
||||
|
|
@ -83,13 +83,25 @@ export default function ParticipantList({
|
|||
return `${(distance / 1000).toFixed(1)}km`;
|
||||
};
|
||||
|
||||
const currentParticipant = participants.find((p) => p.name === currentUserId);
|
||||
// Deduplicate participants by name, keeping the most recently seen one
|
||||
const deduplicatedParticipants = useMemo(() => {
|
||||
const byName = new Map<string, typeof participants[0]>();
|
||||
for (const p of participants) {
|
||||
const existing = byName.get(p.name);
|
||||
if (!existing || p.lastSeen > existing.lastSeen) {
|
||||
byName.set(p.name, p);
|
||||
}
|
||||
}
|
||||
return Array.from(byName.values());
|
||||
}, [participants]);
|
||||
|
||||
const currentParticipant = deduplicatedParticipants.find((p) => p.name === currentUserId);
|
||||
|
||||
return (
|
||||
<div className="room-panel h-full md:h-auto md:max-h-[calc(100vh-4rem)] rounded-t-2xl md:rounded-2xl md:m-4 overflow-hidden flex flex-col">
|
||||
{/* Header */}
|
||||
<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 ({deduplicatedParticipants.length})</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Refresh locations button */}
|
||||
{syncUrl && (
|
||||
|
|
@ -139,13 +151,13 @@ export default function ParticipantList({
|
|||
|
||||
{/* Participant list */}
|
||||
<div className="flex-1 overflow-y-auto p-2">
|
||||
{participants.length === 0 ? (
|
||||
{deduplicatedParticipants.length === 0 ? (
|
||||
<div className="text-center text-white/40 py-8">
|
||||
No one else is here yet
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{participants.map((participant) => {
|
||||
{deduplicatedParticipants.map((participant) => {
|
||||
const isMe = participant.name === currentUserId;
|
||||
const distance = !isMe
|
||||
? formatDistance(participant, currentParticipant)
|
||||
|
|
|
|||
Loading…
Reference in New Issue