fix: local IP detection regex + add join code UI when no peers found
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 38s

The ICE candidate regex was matching the wrong part of the candidate
string. Also, iOS Safari blocks local IP detection via WebRTC (mDNS
obfuscation), so when no peers are found, show a prominent "create
link" button and a join-by-code input for easy cross-network pairing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ordinarthur 2026-04-14 12:34:01 +02:00
parent 99a182a831
commit 2e3408e8d7
3 changed files with 64 additions and 12 deletions

View File

@ -1,23 +1,73 @@
import { useState } from "react";
import { useStore } from "../stores/useStore";
import PeerAvatar from "./PeerAvatar";
interface PeerListProps {
onPeerSelect: (peerId: string) => void;
onCreateRoom?: () => void;
}
export default function PeerList({ onPeerSelect }: PeerListProps) {
export default function PeerList({ onPeerSelect, onCreateRoom }: PeerListProps) {
const peers = useStore((s) => s.peers);
const selectedPeerId = useStore((s) => s.selectedPeerId);
const [joinCode, setJoinCode] = useState("");
if (peers.length === 0) {
return (
<div className="flex flex-col items-center justify-center py-16 text-slate-500">
<div className="flex flex-col items-center justify-center py-12 text-slate-500">
<div className="text-5xl mb-4">📡</div>
<p className="text-lg font-medium">En attente d'appareils...</p>
<p className="text-sm mt-2 text-center max-w-xs">
Ouvrez AnyDrop sur un autre appareil connecté au même Wi-Fi,
ou partagez un lien public.
Ouvrez AnyDrop sur un autre appareil connecté au même Wi-Fi.
</p>
<div className="mt-6 w-full max-w-xs space-y-3">
<p className="text-xs text-slate-600 text-center">Pas sur le même réseau ?</p>
{onCreateRoom && (
<button
onClick={onCreateRoom}
className="w-full flex items-center justify-center gap-2 px-4 py-2.5
border border-slate-700 hover:border-brand-500 rounded-xl
text-slate-300 hover:text-white transition-all
bg-slate-900/30 hover:bg-slate-900/50"
>
<span>🔗</span>
<span className="text-sm font-medium">Créer un lien de partage</span>
</button>
)}
<div className="flex gap-2">
<input
type="text"
value={joinCode}
onChange={(e) => setJoinCode(e.target.value.toLowerCase().trim())}
placeholder="Code de partage"
maxLength={4}
className="flex-1 px-3 py-2.5 bg-slate-800/50 border border-slate-700
rounded-xl text-white text-sm text-center font-mono tracking-widest
placeholder:text-slate-600 focus:outline-none focus:border-brand-500"
onKeyDown={(e) => {
if (e.key === "Enter" && joinCode.length >= 3) {
window.location.href = `/${joinCode}`;
}
}}
/>
<button
onClick={() => {
if (joinCode.length >= 3) {
window.location.href = `/${joinCode}`;
}
}}
disabled={joinCode.length < 3}
className="px-4 py-2.5 bg-brand-500 hover:bg-brand-400 disabled:opacity-30
disabled:cursor-not-allowed rounded-xl text-white text-sm font-medium
transition-colors"
>
Rejoindre
</button>
</div>
</div>
</div>
);
}

View File

@ -26,13 +26,15 @@ export async function detectLocalIP(timeoutMs = 3000): Promise<string | undefine
pc.onicecandidate = (event) => {
if (!event.candidate?.candidate) return;
const match = event.candidate.candidate.match(
/(?:srflx|host)\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s/,
);
if (match) {
const ip = match[1];
if (isPrivateIPv4(ip)) {
done(ip);
// ICE candidate format: "candidate:<foundation> <component> <transport> <priority> <ip> <port> typ <type> ..."
// Extract all IPv4 addresses from the candidate string
const ips = event.candidate.candidate.match(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g);
if (ips) {
for (const ip of ips) {
if (isPrivateIPv4(ip)) {
done(ip);
return;
}
}
}
};

View File

@ -124,7 +124,7 @@ function HomeConnected() {
{/* Peer list */}
<section className="mb-6">
<PeerList onPeerSelect={handlePeerSelect} />
<PeerList onPeerSelect={handlePeerSelect} onCreateRoom={createPublicRoom} />
</section>
{/* Drop zone */}