From 91ddfddf53603d3d5b392ea141d66c5a7e64ceed Mon Sep 17 00:00:00 2001 From: Jure <44338+hoornet@users.noreply.github.com> Date: Tue, 31 Mar 2026 06:45:55 +0200 Subject: [PATCH] Fix relay dedup: normalize URLs to prevent trailing-slash duplicates --- src/components/shared/RelaysView.tsx | 10 +++++----- src/lib/nostr/core.ts | 28 ++++++++++++++++++++++------ src/lib/nostr/index.ts | 2 +- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/components/shared/RelaysView.tsx b/src/components/shared/RelaysView.tsx index 4c0b361..5070f44 100644 --- a/src/components/shared/RelaysView.tsx +++ b/src/components/shared/RelaysView.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { getNDK, getStoredRelayUrls, addRelay, removeRelay, publishRelayList, fetchRelayRecommendations } from "../../lib/nostr"; +import { getNDK, getStoredRelayUrls, addRelay, removeRelay, publishRelayList, fetchRelayRecommendations, normalizeRelayUrl } from "../../lib/nostr"; import { useRelayHealthStore } from "../../stores/relayHealth"; import { useUserStore } from "../../stores/user"; import type { RelayHealthResult } from "../../lib/nostr/relayHealth"; @@ -154,7 +154,7 @@ export function RelaysView() { const { loggedIn } = useUserStore(); const ndk = getNDK(); const poolRelays = Array.from(ndk.pool?.relays?.values() ?? []); - const poolConnectedUrls = new Set(poolRelays.filter((r) => r.connected).map((r) => r.url)); + const poolConnectedUrls = new Set(poolRelays.filter((r) => r.connected).map((r) => normalizeRelayUrl(r.url))); const [input, setInput] = useState(""); const [addError, setAddError] = useState(null); @@ -180,7 +180,7 @@ export function RelaysView() { setAddError("URL must start with ws:// or wss://"); return; } - if (getStoredRelayUrls().includes(url)) { + if (getStoredRelayUrls().includes(normalizeRelayUrl(url))) { setAddError("Already in list"); return; } @@ -218,8 +218,8 @@ export function RelaysView() { }; // Merge: show health results first, then any pool relays not yet checked - const checkedUrls = new Set(results.map((r) => r.url)); - const uncheckedPoolRelays = poolRelays.filter((r) => !checkedUrls.has(r.url)); + const checkedUrls = new Set(results.map((r) => normalizeRelayUrl(r.url))); + const uncheckedPoolRelays = poolRelays.filter((r) => !checkedUrls.has(normalizeRelayUrl(r.url))); // Sort: online first, then slow, then offline const sortedResults = [...results].sort((a, b) => { diff --git a/src/lib/nostr/core.ts b/src/lib/nostr/core.ts index 77fd6b0..60a3667 100644 --- a/src/lib/nostr/core.ts +++ b/src/lib/nostr/core.ts @@ -51,16 +51,30 @@ export const OUTBOX_RELAYS = [ "wss://relay.nostr.band/", ]; +/** Normalize relay URL: lowercase host, strip trailing slash, deduplicate. */ +export function normalizeRelayUrl(url: string): string { + return url.replace(/\/+$/, ""); +} + export function getStoredRelayUrls(): string[] { try { const stored = localStorage.getItem(RELAY_STORAGE_KEY); - if (stored) return JSON.parse(stored); + if (stored) { + // Deduplicate on load (handles legacy duplicates from trailing-slash mismatch) + const urls: string[] = JSON.parse(stored); + const seen = new Set(); + return urls.map(normalizeRelayUrl).filter((u) => { + if (seen.has(u)) return false; + seen.add(u); + return true; + }); + } } catch { /* ignore */ } return FALLBACK_RELAYS; } export function saveRelayUrls(urls: string[]) { - localStorage.setItem(RELAY_STORAGE_KEY, JSON.stringify(urls)); + localStorage.setItem(RELAY_STORAGE_KEY, JSON.stringify(urls.map(normalizeRelayUrl))); } let ndk: NDK | null = null; @@ -119,13 +133,15 @@ export async function resetNDK(): Promise { } export function addRelay(url: string): void { + const normalized = normalizeRelayUrl(url); const instance = getNDK(); const urls = getStoredRelayUrls(); - if (!urls.includes(url)) { - saveRelayUrls([...urls, url]); + if (!urls.includes(normalized)) { + saveRelayUrls([...urls, normalized]); } - if (!instance.pool?.relays.has(url)) { - const relay = new NDKRelay(url, undefined, instance); + // Check both with and without trailing slash since NDK may use either + if (!instance.pool?.relays.has(normalized) && !instance.pool?.relays.has(normalized + "/")) { + const relay = new NDKRelay(normalized, undefined, instance); instance.pool?.addRelay(relay, true); } } diff --git a/src/lib/nostr/index.ts b/src/lib/nostr/index.ts index f381ff3..5964bc0 100644 --- a/src/lib/nostr/index.ts +++ b/src/lib/nostr/index.ts @@ -1,4 +1,4 @@ -export { getNDK, getNDKUptimeMs, connectToRelays, ensureConnected, resetNDK, getStoredRelayUrls, addRelay, removeRelay, fetchWithTimeout, withTimeout, FEED_TIMEOUT, THREAD_TIMEOUT, SINGLE_TIMEOUT } from "./core"; +export { getNDK, getNDKUptimeMs, connectToRelays, ensureConnected, resetNDK, getStoredRelayUrls, normalizeRelayUrl, addRelay, removeRelay, fetchWithTimeout, withTimeout, FEED_TIMEOUT, THREAD_TIMEOUT, SINGLE_TIMEOUT } from "./core"; export { fetchGlobalFeed, fetchFollowFeed, fetchUserNotes, fetchUserNotesNIP65, fetchNoteById, fetchReplies, publishNote, publishReply, publishRepost, publishQuote, fetchHashtagFeed, fetchThreadEvents, fetchAncestors } from "./notes"; export { publishProfile, publishContactList, fetchProfile, fetchFollowSuggestions, fetchMentions, fetchFollowers, fetchNewFollowers } from "./social"; export { publishArticle, fetchArticle, fetchAuthorArticles, fetchArticleFeed, searchArticles, fetchByAddr } from "./articles";