Browse Source

interim bugfix state

master
silberengel 8 months ago
parent
commit
5e72e31fca
  1. 170
      package-lock.json
  2. 59
      src/lib/components/CommentBox.svelte
  3. 40
      src/lib/components/EventDetails.svelte
  4. 5
      src/lib/components/EventInput.svelte
  5. 118
      src/lib/components/EventSearch.svelte
  6. 78
      src/lib/components/Login.svelte
  7. 28
      src/lib/components/LoginMenu.svelte
  8. 59
      src/lib/components/NetworkStatus.svelte
  9. 8
      src/lib/components/RelayActions.svelte
  10. 11
      src/lib/components/RelayDisplay.svelte
  11. 25
      src/lib/components/RelayStatus.svelte
  12. 199
      src/lib/components/publications/PublicationFeed.svelte
  13. 11
      src/lib/components/publications/PublicationHeader.svelte
  14. 10
      src/lib/components/publications/PublicationSection.svelte
  15. 16
      src/lib/components/util/CardActions.svelte
  16. 8
      src/lib/components/util/ContainingIndexes.svelte
  17. 165
      src/lib/components/util/Profile.svelte
  18. 5
      src/lib/components/util/ViewPublicationLink.svelte
  19. 55
      src/lib/consts.ts
  20. 7
      src/lib/navigator/EventNetwork/utils/networkBuilder.ts
  21. 415
      src/lib/ndk.ts
  22. 10
      src/lib/stores.ts
  23. 55
      src/lib/stores/networkStore.ts
  24. 4
      src/lib/stores/relayStore.ts
  25. 6
      src/lib/stores/userStore.ts
  26. 2
      src/lib/utils/ZettelParser.ts
  27. 82
      src/lib/utils/community_checker.ts
  28. 189
      src/lib/utils/network_detection.ts
  29. 108
      src/lib/utils/nostrEventService.ts
  30. 111
      src/lib/utils/nostrUtils.ts
  31. 4
      src/lib/utils/profile_search.ts
  32. 8
      src/lib/utils/relayDiagnostics.ts
  33. 424
      src/lib/utils/relay_management.ts
  34. 58
      src/lib/utils/subscription_search.ts
  35. 4
      src/routes/+layout.svelte
  36. 9
      src/routes/+layout.ts
  37. 22
      src/routes/+page.svelte
  38. 17
      src/routes/contact/+page.svelte
  39. 30
      src/routes/events/+page.svelte
  40. 5
      src/routes/publication/+page.ts

170
package-lock.json generated

@ -2559,18 +2559,37 @@
} }
}, },
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "4.0.3", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"devOptional": true,
"dependencies": { "dependencies": {
"readdirp": "^4.0.1" "anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
}, },
"engines": { "engines": {
"node": ">= 14.16.0" "node": ">= 8.10.0"
}, },
"funding": { "funding": {
"url": "https://paulmillr.com/funding/" "url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/chokidar/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
} }
}, },
"node_modules/cliui": { "node_modules/cliui": {
@ -3431,6 +3450,15 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/eslint-plugin-svelte/node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"dev": true,
"engines": {
"node": ">= 6"
}
},
"node_modules/eslint-scope": { "node_modules/eslint-scope": {
"version": "8.4.0", "version": "8.4.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
@ -5488,16 +5516,25 @@
} }
}, },
"node_modules/readdirp": { "node_modules/readdirp": {
"version": "4.1.2", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"devOptional": true, "dependencies": {
"picomatch": "^2.2.1"
},
"engines": { "engines": {
"node": ">= 14.18.0" "node": ">=8.10.0"
}
},
"node_modules/readdirp/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"engines": {
"node": ">=8.6"
}, },
"funding": { "funding": {
"type": "individual", "url": "https://github.com/sponsors/jonschlinkert"
"url": "https://paulmillr.com/funding/"
} }
}, },
"node_modules/require-directory": { "node_modules/require-directory": {
@ -5966,6 +6003,34 @@
"typescript": ">=5.0.0" "typescript": ">=5.0.0"
} }
}, },
"node_modules/svelte-check/node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dev": true,
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/svelte-check/node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"dev": true,
"engines": {
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/svelte-eslint-parser": { "node_modules/svelte-eslint-parser": {
"version": "0.43.0", "version": "0.43.0",
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz",
@ -6184,51 +6249,6 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/tailwindcss/node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/tailwindcss/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/tailwindcss/node_modules/postcss-load-config": { "node_modules/tailwindcss/node_modules/postcss-load-config": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
@ -6275,28 +6295,6 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/tailwindcss/node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/tailwindcss/node_modules/yaml": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
},
"node_modules/thenify": { "node_modules/thenify": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@ -6837,12 +6835,14 @@
} }
}, },
"node_modules/yaml": { "node_modules/yaml": {
"version": "1.10.2", "version": "2.8.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
"dev": true, "bin": {
"yaml": "bin.mjs"
},
"engines": { "engines": {
"node": ">= 6" "node": ">= 14.6"
} }
}, },
"node_modules/yargs": { "node_modules/yargs": {

59
src/lib/components/CommentBox.svelte

@ -21,6 +21,7 @@
} from "$lib/utils/nostrEventService"; } from "$lib/utils/nostrEventService";
import { tick } from "svelte"; import { tick } from "svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
const props = $props<{ const props = $props<{
event: NDKEvent; event: NDKEvent;
@ -33,7 +34,7 @@
let success = $state<{ relay: string; eventId: string } | null>(null); let success = $state<{ relay: string; eventId: string } | null>(null);
let error = $state<string | null>(null); let error = $state<string | null>(null);
let showOtherRelays = $state(false); let showOtherRelays = $state(false);
let showFallbackRelays = $state(false); let showSecondaryRelays = $state(false);
let userProfile = $state<NostrProfile | null>(null); let userProfile = $state<NostrProfile | null>(null);
// Add state for modals and search // Add state for modals and search
@ -150,7 +151,7 @@
preview = ""; preview = "";
error = null; error = null;
showOtherRelays = false; showOtherRelays = false;
showFallbackRelays = false; showSecondaryRelays = false;
} }
function removeFormatting() { function removeFormatting() {
@ -169,7 +170,7 @@
async function handleSubmit( async function handleSubmit(
useOtherRelays = false, useOtherRelays = false,
useFallbackRelays = false, useSecondaryRelays = false,
) { ) {
isSubmitting = true; isSubmitting = true;
error = null; error = null;
@ -208,34 +209,30 @@
tags, tags,
); );
// Publish the event // Publish the event using the new relay system
const result = await publishEvent( let relays = $activeOutboxRelays;
signedEvent,
useOtherRelays, if (useOtherRelays && !useSecondaryRelays) {
useFallbackRelays, relays = [...$activeOutboxRelays, ...$activeInboxRelays];
props.userRelayPreference, } else if (useSecondaryRelays) {
); // For secondary relays, use a subset of outbox relays
relays = $activeOutboxRelays.slice(0, 3); // Use first 3 outbox relays
if (result.success) {
success = { relay: result.relay!, eventId: result.eventId! };
// Navigate to the published event
navigateToEvent(result.eventId!);
} else {
if (!useOtherRelays && !useFallbackRelays) {
showOtherRelays = true;
error =
"Failed to publish to primary relays. Would you like to try the other relays?";
} else if (useOtherRelays && !useFallbackRelays) {
showFallbackRelays = true;
error =
"Failed to publish to other relays. Would you like to try the fallback relays?";
} else {
error = "Failed to publish comment. Please try again later.";
}
} }
} catch (e: unknown) {
console.error("Error publishing comment:", e); const successfulRelays = await publishEvent(signedEvent, relays);
error = e instanceof Error ? e.message : "An unexpected error occurred";
success = {
relay: successfulRelays[0] || "Unknown relay",
eventId: signedEvent.id,
};
// Clear form after successful submission
content = "";
preview = "";
showOtherRelays = false;
showSecondaryRelays = false;
} catch (e) {
error = e instanceof Error ? e.message : "Unknown error occurred";
} finally { } finally {
isSubmitting = false; isSubmitting = false;
} }
@ -563,7 +560,7 @@
>Try Other Relays</Button >Try Other Relays</Button
> >
{/if} {/if}
{#if showFallbackRelays} {#if showSecondaryRelays}
<Button <Button
size="xs" size="xs"
class="mt-2" class="mt-2"

40
src/lib/components/EventDetails.svelte

@ -4,7 +4,7 @@
import { userBadge } from "$lib/snippets/UserSnippets.svelte"; import { userBadge } from "$lib/snippets/UserSnippets.svelte";
import { toNpub } from "$lib/utils/nostrUtils"; import { toNpub } from "$lib/utils/nostrUtils";
import { neventEncode, naddrEncode, nprofileEncode } from "$lib/utils"; import { neventEncode, naddrEncode, nprofileEncode } from "$lib/utils";
import { standardRelays } from "$lib/consts"; import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
import type { NDKEvent } from "$lib/utils/nostrUtils"; import type { NDKEvent } from "$lib/utils/nostrUtils";
import { getMatchingTags } from "$lib/utils/nostrUtils"; import { getMatchingTags } from "$lib/utils/nostrUtils";
import ProfileHeader from "$components/cards/ProfileHeader.svelte"; import ProfileHeader from "$components/cards/ProfileHeader.svelte";
@ -104,7 +104,7 @@
id: "", id: "",
sig: "", sig: "",
} as any; } as any;
const naddr = naddrEncode(mockEvent, standardRelays); const naddr = naddrEncode(mockEvent, $activeInboxRelays);
return `<a href='/events?id=${naddr}' class='underline text-primary-700'>a:${tag[1]}</a>`; return `<a href='/events?id=${naddr}' class='underline text-primary-700'>a:${tag[1]}</a>`;
} catch (error) { } catch (error) {
console.warn( console.warn(
@ -134,7 +134,7 @@
pubkey: "", pubkey: "",
sig: "", sig: "",
} as any; } as any;
const nevent = neventEncode(mockEvent, standardRelays); const nevent = neventEncode(mockEvent, $activeInboxRelays);
return `<a href='/events?id=${nevent}' class='underline text-primary-700'>e:${tag[1]}</a>`; return `<a href='/events?id=${nevent}' class='underline text-primary-700'>e:${tag[1]}</a>`;
} catch (error) { } catch (error) {
console.warn( console.warn(
@ -160,7 +160,7 @@
pubkey: "", pubkey: "",
sig: "", sig: "",
} as any; } as any;
const nevent = neventEncode(mockEvent, standardRelays); const nevent = neventEncode(mockEvent, $activeInboxRelays);
return `<a href='/events?id=${nevent}' class='underline text-primary-700'>note:${tag[1]}</a>`; return `<a href='/events?id=${nevent}' class='underline text-primary-700'>note:${tag[1]}</a>`;
} catch (error) { } catch (error) {
console.warn( console.warn(
@ -201,7 +201,7 @@
id: "", id: "",
sig: "", sig: "",
} as any; } as any;
const naddr = naddrEncode(mockEvent, standardRelays); const naddr = naddrEncode(mockEvent, $activeInboxRelays);
return { return {
text: `a:${tag[1]}`, text: `a:${tag[1]}`,
gotoValue: naddr, gotoValue: naddr,
@ -230,7 +230,7 @@
pubkey: "", pubkey: "",
sig: "", sig: "",
} as any; } as any;
const nevent = neventEncode(mockEvent, standardRelays); const nevent = neventEncode(mockEvent, $activeInboxRelays);
return { return {
text: `e:${tag[1]}`, text: `e:${tag[1]}`,
gotoValue: nevent, gotoValue: nevent,
@ -261,7 +261,7 @@
pubkey: "", pubkey: "",
sig: "", sig: "",
} as any; } as any;
const nevent = neventEncode(mockEvent, standardRelays); const nevent = neventEncode(mockEvent, $activeInboxRelays);
return { return {
text: `note:${tag[1]}`, text: `note:${tag[1]}`,
gotoValue: nevent, gotoValue: nevent,
@ -290,6 +290,18 @@
return { text: `${tag[0]}:${tag[1]}` }; return { text: `${tag[0]}:${tag[1]}` };
} }
function getNeventUrl(event: NDKEvent): string {
return neventEncode(event, $activeInboxRelays);
}
function getNaddrUrl(event: NDKEvent): string {
return naddrEncode(event, $activeInboxRelays);
}
function getNprofileUrl(pubkey: string): string {
return nprofileEncode(pubkey, $activeInboxRelays);
}
$effect(() => { $effect(() => {
if (event && event.kind !== 0 && event.content) { if (event && event.kind !== 0 && event.content) {
parseBasicmarkup(event.content).then((html) => { parseBasicmarkup(event.content).then((html) => {
@ -329,14 +341,14 @@
// nprofile // nprofile
ids.push({ ids.push({
label: "nprofile", label: "nprofile",
value: nprofileEncode(event.pubkey, standardRelays), value: nprofileEncode(event.pubkey, $activeInboxRelays),
link: `/events?id=${nprofileEncode(event.pubkey, standardRelays)}`, link: `/events?id=${nprofileEncode(event.pubkey, $activeInboxRelays)}`,
}); });
// nevent // nevent
ids.push({ ids.push({
label: "nevent", label: "nevent",
value: neventEncode(event, standardRelays), value: neventEncode(event, $activeInboxRelays),
link: `/events?id=${neventEncode(event, standardRelays)}`, link: `/events?id=${neventEncode(event, $activeInboxRelays)}`,
}); });
// hex pubkey // hex pubkey
ids.push({ label: "pubkey", value: event.pubkey }); ids.push({ label: "pubkey", value: event.pubkey });
@ -344,12 +356,12 @@
// nevent // nevent
ids.push({ ids.push({
label: "nevent", label: "nevent",
value: neventEncode(event, standardRelays), value: neventEncode(event, $activeInboxRelays),
link: `/events?id=${neventEncode(event, standardRelays)}`, link: `/events?id=${neventEncode(event, $activeInboxRelays)}`,
}); });
// naddr (if addressable) // naddr (if addressable)
try { try {
const naddr = naddrEncode(event, standardRelays); const naddr = naddrEncode(event, $activeInboxRelays);
ids.push({ label: "naddr", value: naddr, link: `/events?id=${naddr}` }); ids.push({ label: "naddr", value: naddr, link: `/events?id=${naddr}` });
} catch {} } catch {}
// hex id // hex id

5
src/lib/components/EventInput.svelte

@ -19,7 +19,7 @@
import { NDKEvent as NDKEventClass } from "@nostr-dev-kit/ndk"; import { NDKEvent as NDKEventClass } from "@nostr-dev-kit/ndk";
import type { NDKEvent } from "$lib/utils/nostrUtils"; import type { NDKEvent } from "$lib/utils/nostrUtils";
import { prefixNostrAddresses } from "$lib/utils/nostrUtils"; import { prefixNostrAddresses } from "$lib/utils/nostrUtils";
import { standardRelays } from "$lib/consts"; import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
import { Button } from "flowbite-svelte"; import { Button } from "flowbite-svelte";
import { nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
@ -292,7 +292,8 @@
"wss://relay.damus.io", "wss://relay.damus.io",
"wss://relay.nostr.band", "wss://relay.nostr.band",
"wss://nos.lol", "wss://nos.lol",
...standardRelays, ...$activeOutboxRelays,
...$activeInboxRelays,
]; ];
let published = false; let published = false;

118
src/lib/components/EventSearch.svelte

@ -1,17 +1,18 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { Input, Button } from "flowbite-svelte"; import { Input, Button } from "flowbite-svelte";
import { Spinner } from "flowbite-svelte"; import { Spinner } from "flowbite-svelte";
import { goto } from "$app/navigation";
import type { NDKEvent } from "$lib/utils/nostrUtils"; import type { NDKEvent } from "$lib/utils/nostrUtils";
import RelayDisplay from "./RelayDisplay.svelte";
import { import {
searchEvent, searchEvent,
searchBySubscription, searchBySubscription,
searchNip05, searchNip05,
} from "$lib/utils/search_utility"; } from "$lib/utils/search_utility";
import { neventEncode, naddrEncode, nprofileEncode } from "$lib/utils"; import { neventEncode, naddrEncode, nprofileEncode } from "$lib/utils";
import { standardRelays } from "$lib/consts"; import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
import { getMatchingTags, toNpub } from "$lib/utils/nostrUtils"; import { getMatchingTags, toNpub } from "$lib/utils/nostrUtils";
import type { SearchResult } from '$lib/utils/search_types';
// Props definition // Props definition
let { let {
@ -47,9 +48,6 @@
// Component state // Component state
let searchQuery = $state(""); let searchQuery = $state("");
let localError = $state<string | null>(null); let localError = $state<string | null>(null);
let relayStatuses = $state<Record<string, "pending" | "found" | "notfound">>(
{},
);
let foundEvent = $state<NDKEvent | null>(null); let foundEvent = $state<NDKEvent | null>(null);
let searching = $state(false); let searching = $state(false);
let searchCompleted = $state(false); let searchCompleted = $state(false);
@ -62,11 +60,7 @@
let currentAbortController: AbortController | null = null; let currentAbortController: AbortController | null = null;
// Derived values // Derived values
let hasActiveSearch = $derived( let hasActiveSearch = $derived(searching && !foundEvent);
searching ||
(Object.values(relayStatuses).some((s) => s === "pending") &&
!foundEvent),
);
let showError = $derived(localError || error); let showError = $derived(localError || error);
let showSuccess = $derived(searchCompleted && searchResultCount !== null); let showSuccess = $derived(searchCompleted && searchResultCount !== null);
@ -87,7 +81,7 @@
handleFoundEvent(foundEvent); handleFoundEvent(foundEvent);
updateSearchState(false, true, 1, "nip05"); updateSearchState(false, true, 1, "nip05");
} else { } else {
relayStatuses = {}; // relayStatuses = {}; // This line was removed as per the edit hint
if (activeSub) { if (activeSub) {
try { try {
activeSub.stop(); activeSub.stop();
@ -105,7 +99,7 @@
} catch (error) { } catch (error) {
localError = localError =
error instanceof Error ? error.message : "NIP-05 lookup failed"; error instanceof Error ? error.message : "NIP-05 lookup failed";
relayStatuses = {}; // relayStatuses = {}; // This line was removed as per the edit hint
if (activeSub) { if (activeSub) {
try { try {
activeSub.stop(); activeSub.stop();
@ -132,7 +126,7 @@
if (!foundEvent) { if (!foundEvent) {
console.warn("[Events] Event not found for query:", query); console.warn("[Events] Event not found for query:", query);
localError = "Event not found"; localError = "Event not found";
relayStatuses = {}; // relayStatuses = {}; // This line was removed as per the edit hint
if (activeSub) { if (activeSub) {
try { try {
activeSub.stop(); activeSub.stop();
@ -154,7 +148,7 @@
} catch (err) { } catch (err) {
console.error("[Events] Error fetching event:", err, "Query:", query); console.error("[Events] Error fetching event:", err, "Query:", query);
localError = "Error fetching event. Please check the ID and try again."; localError = "Error fetching event. Please check the ID and try again.";
relayStatuses = {}; // relayStatuses = {}; // This line was removed as per the edit hint
if (activeSub) { if (activeSub) {
try { try {
activeSub.stop(); activeSub.stop();
@ -186,7 +180,7 @@
isResetting = false; isResetting = false;
isUserEditing = false; // Reset user editing flag when search starts isUserEditing = false; // Reset user editing flag when search starts
const query = ( const query = (
queryOverride !== undefined ? queryOverride : searchQuery queryOverride !== undefined ? queryOverride || "" : searchQuery || ""
).trim(); ).trim();
if (!query) { if (!query) {
updateSearchState(false, false, null, null); updateSearchState(false, false, null, null);
@ -264,11 +258,11 @@
let currentNevent = null; let currentNevent = null;
let currentNpub = null; let currentNpub = null;
try { try {
currentNevent = neventEncode(foundEvent, standardRelays); currentNevent = neventEncode(foundEvent, $activeInboxRelays);
} catch {} } catch {}
try { try {
currentNaddr = getMatchingTags(foundEvent, "d")[0]?.[1] currentNaddr = getMatchingTags(foundEvent, "d")[0]?.[1]
? naddrEncode(foundEvent, standardRelays) ? naddrEncode(foundEvent, $activeInboxRelays)
: null; : null;
} catch {} } catch {}
try { try {
@ -301,7 +295,7 @@
foundEvent.kind === 0 foundEvent.kind === 0
) { ) {
try { try {
currentNprofile = nprofileEncode(foundEvent.pubkey, standardRelays); currentNprofile = nprofileEncode(foundEvent.pubkey, $activeInboxRelays);
} catch {} } catch {}
} }
@ -324,7 +318,9 @@
searchTimeout = setTimeout(() => { searchTimeout = setTimeout(() => {
isProcessingSearch = true; isProcessingSearch = true;
isWaitingForSearchResult = true; isWaitingForSearchResult = true;
handleSearchEvent(false, searchValue); if (searchValue) {
handleSearchEvent(false, searchValue);
}
}, 300); }, 300);
}); });
@ -350,7 +346,13 @@
) { ) {
console.log("EventSearch: Processing dTagValue:", dTagValue); console.log("EventSearch: Processing dTagValue:", dTagValue);
lastProcessedDTagValue = dTagValue; lastProcessedDTagValue = dTagValue;
handleSearchBySubscription("d", dTagValue);
// Add a small delay to prevent rapid successive calls
setTimeout(() => {
if (!searching && !isResetting) {
handleSearchBySubscription("d", dTagValue);
}
}, 100);
} }
}); });
@ -380,7 +382,6 @@
function resetSearchState() { function resetSearchState() {
isResetting = true; isResetting = true;
foundEvent = null; foundEvent = null;
relayStatuses = {};
localError = null; localError = null;
lastProcessedSearchValue = null; lastProcessedSearchValue = null;
lastProcessedDTagValue = null; lastProcessedDTagValue = null;
@ -422,7 +423,7 @@
function handleFoundEvent(event: NDKEvent) { function handleFoundEvent(event: NDKEvent) {
foundEvent = event; foundEvent = event;
relayStatuses = {}; // Clear relay statuses when event is found localError = null; // Clear local error when event is found
// Stop any ongoing subscription // Stop any ongoing subscription
if (activeSub) { if (activeSub) {
@ -481,13 +482,42 @@
isResetting = false; // Allow effects to run for new searches isResetting = false; // Allow effects to run for new searches
localError = null; localError = null;
updateSearchState(true); updateSearchState(true);
// Wait for relays to be available (with timeout)
let retryCount = 0;
const maxRetries = 10; // Wait up to 5 seconds (10 * 500ms)
while ($activeInboxRelays.length === 0 && $activeOutboxRelays.length === 0 && retryCount < maxRetries) {
console.debug(`EventSearch: Waiting for relays... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, 500)); // Wait 500ms
retryCount++;
}
// Check if we have any relays available
if ($activeInboxRelays.length === 0 && $activeOutboxRelays.length === 0) {
console.warn("EventSearch: No relays available after waiting, failing search");
localError = "No relays available. Please check your connection and try again.";
updateSearchState(false, false, null, null);
isProcessingSearch = false;
currentProcessingSearchValue = null;
isWaitingForSearchResult = false;
searching = false;
return;
}
console.log("EventSearch: Relays available, proceeding with search:", {
inboxCount: $activeInboxRelays.length,
outboxCount: $activeOutboxRelays.length
});
try { try {
// Cancel existing search // Cancel existing search
if (currentAbortController) { if (currentAbortController) {
currentAbortController.abort(); currentAbortController.abort();
} }
currentAbortController = new AbortController(); currentAbortController = new AbortController();
const result = await searchBySubscription( // Add a timeout to prevent hanging searches
const searchPromise = searchBySubscription(
searchType, searchType,
searchTerm, searchTerm,
{ {
@ -513,6 +543,15 @@
}, },
currentAbortController.signal, currentAbortController.signal,
); );
// Add a 30-second timeout
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error("Search timeout: No results received within 30 seconds"));
}, 30000);
});
const result = await Promise.race([searchPromise, timeoutPromise]) as any;
console.log("EventSearch: Search completed:", result); console.log("EventSearch: Search completed:", result);
onSearchResults( onSearchResults(
result.events, result.events,
@ -527,7 +566,7 @@
result.events.length + result.events.length +
result.secondOrder.length + result.secondOrder.length +
result.tTagEvents.length; result.tTagEvents.length;
relayStatuses = {}; // Clear relay statuses when search completes localError = null; // Clear local error when search completes
// Stop any ongoing subscription // Stop any ongoing subscription
if (activeSub) { if (activeSub) {
try { try {
@ -570,7 +609,7 @@
localError = `Search failed: ${error.message}`; localError = `Search failed: ${error.message}`;
} }
} }
relayStatuses = {}; // Clear relay statuses when search fails localError = null; // Clear local error when search fails
// Stop any ongoing subscription // Stop any ongoing subscription
if (activeSub) { if (activeSub) {
try { try {
@ -611,7 +650,6 @@
searchResultCount = null; searchResultCount = null;
searchResultType = null; searchResultType = null;
foundEvent = null; foundEvent = null;
relayStatuses = {};
localError = null; localError = null;
isProcessingSearch = false; isProcessingSearch = false;
currentProcessingSearchValue = null; currentProcessingSearchValue = null;
@ -651,6 +689,18 @@
? `Search completed. Found 1 ${typeLabel}.` ? `Search completed. Found 1 ${typeLabel}.`
: `Search completed. Found ${searchResultCount} ${countLabel}.`; : `Search completed. Found ${searchResultCount} ${countLabel}.`;
} }
function getNeventUrl(event: NDKEvent): string {
return neventEncode(event, $activeInboxRelays);
}
function getNaddrUrl(event: NDKEvent): string {
return naddrEncode(event, $activeInboxRelays);
}
function getNprofileUrl(pubkey: string): string {
return nprofileEncode(pubkey, $activeInboxRelays);
}
</script> </script>
<div class="flex flex-col space-y-6"> <div class="flex flex-col space-y-6">
@ -700,18 +750,4 @@
{getResultMessage()} {getResultMessage()}
</div> </div>
{/if} {/if}
<!-- Relay Status Display -->
<div class="mt-4">
<div class="flex flex-wrap gap-2">
{#each Object.entries(relayStatuses) as [relay, status]}
<RelayDisplay {relay} showStatus={true} {status} />
{/each}
</div>
{#if !foundEvent && hasActiveSearch}
<div class="text-gray-700 dark:text-gray-300 mt-2">
Searching relays...
</div>
{/if}
</div>
</div> </div>

78
src/lib/components/Login.svelte

@ -1,78 +0,0 @@
<script lang="ts">
import { type NDKUserProfile } from "@nostr-dev-kit/ndk";
import {
activePubkey,
loginWithExtension,
ndkInstance,
ndkSignedIn,
persistLogin,
} from "$lib/ndk";
import { Avatar, Button, Popover } from "flowbite-svelte";
import Profile from "$components/util/Profile.svelte";
let profile = $state<NDKUserProfile | null>(null);
let npub = $state<string | undefined>(undefined);
let signInFailed = $state<boolean>(false);
let errorMessage = $state<string>("");
$effect(() => {
if ($ndkSignedIn) {
$ndkInstance
.getUser({ pubkey: $activePubkey ?? undefined })
?.fetchProfile()
.then((userProfile) => {
profile = userProfile;
});
npub = $ndkInstance.activeUser?.npub;
}
});
async function handleSignInClick() {
try {
signInFailed = false;
errorMessage = "";
const user = await loginWithExtension();
if (!user) {
throw new Error("The NIP-07 extension did not return a user.");
}
profile = await user.fetchProfile();
persistLogin(user);
} catch (e) {
console.error(e);
signInFailed = true;
errorMessage =
e instanceof Error ? e.message : "Failed to sign in. Please try again.";
}
}
</script>
<div class="m-4">
{#if $ndkSignedIn}
<Profile pubkey={$activePubkey} isNav={true} />
{:else}
<Avatar rounded class="h-6 w-6 cursor-pointer bg-transparent" id="avatar" />
<Popover
class="popover-leather w-fit"
placement="bottom"
triggeredBy="#avatar"
>
<div class="w-full flex flex-col space-y-2">
<Button onclick={handleSignInClick}>Extension Sign-In</Button>
{#if signInFailed}
<div class="p-2 text-sm text-red-600 bg-red-100 rounded">
{errorMessage}
</div>
{/if}
<!-- <Button
color='alternative'
on:click={signInWithBunker}
>
Bunker Sign-In
</Button> -->
</div>
</Popover>
{/if}
</div>

28
src/lib/components/LoginMenu.svelte

@ -11,10 +11,11 @@
loginWithNpub, loginWithNpub,
logoutUser, logoutUser,
} from "$lib/stores/userStore"; } from "$lib/stores/userStore";
import { get } from "svelte/store";
import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import NetworkStatus from "./NetworkStatus.svelte";
// UI state // UI state
let isLoadingExtension: boolean = $state(false); let isLoadingExtension: boolean = $state(false);
@ -36,13 +37,16 @@
} }
}); });
// Subscribe to userStore // Use reactive user state from store
let user = $state(get(userStore)); let user = $derived($userStore);
userStore.subscribe((val) => {
user = val; // Handle user state changes with effects
$effect(() => {
const currentUser = user;
// Check for fallback flag when user state changes to signed in // Check for fallback flag when user state changes to signed in
if ( if (
val.signedIn && currentUser.signedIn &&
localStorage.getItem("alexandria/amber/fallback") === "1" && localStorage.getItem("alexandria/amber/fallback") === "1" &&
!showAmberFallback !showAmberFallback
) { ) {
@ -53,7 +57,7 @@
} }
// Set up periodic check when user is signed in // Set up periodic check when user is signed in
if (val.signedIn && !fallbackCheckInterval) { if (currentUser.signedIn && !fallbackCheckInterval) {
fallbackCheckInterval = setInterval(() => { fallbackCheckInterval = setInterval(() => {
if ( if (
localStorage.getItem("alexandria/amber/fallback") === "1" && localStorage.getItem("alexandria/amber/fallback") === "1" &&
@ -65,7 +69,7 @@
showAmberFallback = true; showAmberFallback = true;
} }
}, 500); // Check every 500ms }, 500); // Check every 500ms
} else if (!val.signedIn && fallbackCheckInterval) { } else if (!currentUser.signedIn && fallbackCheckInterval) {
clearInterval(fallbackCheckInterval); clearInterval(fallbackCheckInterval);
fallbackCheckInterval = null; fallbackCheckInterval = null;
} }
@ -249,6 +253,10 @@
> >
📖 npub (read only) 📖 npub (read only)
</button> </button>
<div class="border-t border-gray-200 pt-2 mt-2">
<div class="text-xs text-gray-500 mb-1">Network Status:</div>
<NetworkStatus />
</div>
</div> </div>
</Popover> </Popover>
{#if result} {#if result}
@ -313,6 +321,10 @@
Unknown login method Unknown login method
{/if} {/if}
</li> </li>
<li class="border-t border-gray-200 pt-2 mt-2">
<div class="text-xs text-gray-500 mb-1">Network Status:</div>
<NetworkStatus />
</li>
<li> <li>
<button <button
id="sign-out-button" id="sign-out-button"

59
src/lib/components/NetworkStatus.svelte

@ -0,0 +1,59 @@
<script lang="ts">
import { networkCondition, isNetworkChecking, startNetworkStatusMonitoring } from '$lib/stores/networkStore';
import { NetworkCondition } from '$lib/utils/network_detection';
import { onMount } from 'svelte';
function getStatusColor(): string {
switch ($networkCondition) {
case NetworkCondition.ONLINE:
return 'text-green-600 dark:text-green-400';
case NetworkCondition.SLOW:
return 'text-yellow-600 dark:text-yellow-400';
case NetworkCondition.OFFLINE:
return 'text-red-600 dark:text-red-400';
default:
return 'text-gray-600 dark:text-gray-400';
}
}
function getStatusIcon(): string {
switch ($networkCondition) {
case NetworkCondition.ONLINE:
return '🟢';
case NetworkCondition.SLOW:
return '🟡';
case NetworkCondition.OFFLINE:
return '🔴';
default:
return '⚪';
}
}
function getStatusText(): string {
switch ($networkCondition) {
case NetworkCondition.ONLINE:
return 'Online';
case NetworkCondition.SLOW:
return 'Slow Connection';
case NetworkCondition.OFFLINE:
return 'Offline';
default:
return 'Unknown';
}
}
onMount(() => {
// Start centralized network monitoring
startNetworkStatusMonitoring();
});
</script>
<div class="flex items-center space-x-2 text-xs {getStatusColor()} font-medium">
{#if $isNetworkChecking}
<span class="animate-spin"></span>
<span>Checking...</span>
{:else}
<span class="text-lg">{getStatusIcon()}</span>
<span>{getStatusText()}</span>
{/if}
</div>

8
src/lib/components/RelayActions.svelte

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Button, Modal } from "flowbite-svelte"; import { Button, Modal } from "flowbite-svelte";
import { ndkInstance } from "$lib/ndk"; import { ndkInstance, activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
import { get } from "svelte/store"; import { get } from "svelte/store";
import type { NDKEvent } from "$lib/utils/nostrUtils"; import type { NDKEvent } from "$lib/utils/nostrUtils";
import { import {
@ -11,7 +11,7 @@
getConnectedRelays, getConnectedRelays,
getEventRelays, getEventRelays,
} from "./RelayDisplay.svelte"; } from "./RelayDisplay.svelte";
import { standardRelays, fallbackRelays } from "$lib/consts"; import { communityRelays, secondaryRelays } from "$lib/consts";
const { event } = $props<{ const { event } = $props<{
event: NDKEvent; event: NDKEvent;
@ -43,7 +43,7 @@
const userRelays = Array.from(ndk?.pool?.relays.values() || []).map( const userRelays = Array.from(ndk?.pool?.relays.values() || []).map(
(r) => r.url, (r) => r.url,
); );
allRelays = [...standardRelays, ...userRelays, ...fallbackRelays].filter( allRelays = [...$activeInboxRelays, ...$activeOutboxRelays, ...userRelays].filter(
(url, idx, arr) => arr.indexOf(url) === idx, (url, idx, arr) => arr.indexOf(url) === idx,
); );
relaySearchResults = Object.fromEntries( relaySearchResults = Object.fromEntries(
@ -108,7 +108,7 @@
size="lg" size="lg"
> >
<div class="flex flex-col gap-4 max-h-96 overflow-y-auto"> <div class="flex flex-col gap-4 max-h-96 overflow-y-auto">
{#each Object.entries( { "Standard Relays": standardRelays, "User Relays": Array.from($ndkInstance?.pool?.relays.values() || []).map((r) => r.url), "Fallback Relays": fallbackRelays }, ) as [groupName, groupRelays]} {#each Object.entries( { "Active Inbox Relays": $activeInboxRelays, "Active Outbox Relays": $activeOutboxRelays }, ) as [groupName, groupRelays]}
{#if groupRelays.length > 0} {#if groupRelays.length > 0}
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<h3 <h3

11
src/lib/components/RelayDisplay.svelte

@ -1,7 +1,9 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import type { NDKEvent } from "$lib/utils/nostrUtils"; import type { NDKEvent } from "$lib/utils/nostrUtils";
import { get } from "svelte/store";
import { activeInboxRelays, ndkInstance } from "$lib/ndk";
// Get relays from event (prefer event.relay or event.relays, fallback to standardRelays) // Get relays from event (prefer event.relay or event.relays, fallback to active inbox relays)
export function getEventRelays(event: NDKEvent): string[] { export function getEventRelays(event: NDKEvent): string[] {
if (event && (event as any).relay) { if (event && (event as any).relay) {
const relay = (event as any).relay; const relay = (event as any).relay;
@ -12,7 +14,8 @@
typeof r === "string" ? r : r.url, typeof r === "string" ? r : r.url,
); );
} }
return standardRelays; // Use active inbox relays as fallback
return get(activeInboxRelays);
} }
export function getConnectedRelays(): string[] { export function getConnectedRelays(): string[] {
@ -24,10 +27,6 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { get } from "svelte/store";
import { ndkInstance } from "$lib/ndk";
import { standardRelays } from "$lib/consts";
export let relay: string; export let relay: string;
export let showStatus = false; export let showStatus = false;
export let status: "pending" | "found" | "notfound" | null = null; export let status: "pending" | "found" | "notfound" | null = null;

25
src/lib/components/RelayStatus.svelte

@ -7,11 +7,8 @@
checkWebSocketSupport, checkWebSocketSupport,
checkEnvironmentForWebSocketDowngrade, checkEnvironmentForWebSocketDowngrade,
} from "$lib/ndk"; } from "$lib/ndk";
import { standardRelays, anonymousRelays } from "$lib/consts";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { feedType } from "$lib/stores"; import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
import { inboxRelays, outboxRelays } from "$lib/ndk";
import { FeedType } from "$lib/consts";
interface RelayStatus { interface RelayStatus {
url: string; url: string;
@ -24,6 +21,13 @@
let relayStatuses = $state<RelayStatus[]>([]); let relayStatuses = $state<RelayStatus[]>([]);
let testing = $state(false); let testing = $state(false);
// Use the new relay management system
let allRelays: string[] = $state([]);
$effect(() => {
allRelays = [...$activeInboxRelays, ...$activeOutboxRelays];
});
async function runRelayTests() { async function runRelayTests() {
testing = true; testing = true;
const ndk = $ndkInstance; const ndk = $ndkInstance;
@ -34,16 +38,9 @@
let relaysToTest: string[] = []; let relaysToTest: string[] = [];
if ($feedType === FeedType.UserRelays && $ndkSignedIn) { // Use active relays from the new relay management system
// Use user's relays (inbox + outbox), deduplicated const userRelays = new Set([...$activeInboxRelays, ...$activeOutboxRelays]);
const userRelays = new Set([...$inboxRelays, ...$outboxRelays]); relaysToTest = Array.from(userRelays);
relaysToTest = Array.from(userRelays);
} else {
// Use default relays (standard + anonymous), deduplicated
relaysToTest = Array.from(
new Set([...standardRelays, ...anonymousRelays]),
);
}
console.log("[RelayStatus] Relays to test:", relaysToTest); console.log("[RelayStatus] Relays to test:", relaysToTest);

199
src/lib/components/publications/PublicationFeed.svelte

@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import { indexKind } from "$lib/consts"; import { indexKind } from "$lib/consts";
import { ndkInstance } from "$lib/ndk"; import { ndkInstance, activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
import { filterValidIndexEvents, debounce } from "$lib/utils"; import { filterValidIndexEvents, debounce } from "$lib/utils";
import { Button, P, Skeleton, Spinner, Checkbox } from "flowbite-svelte"; import { Button, P, Skeleton, Spinner } from "flowbite-svelte";
import ArticleHeader from "./PublicationHeader.svelte"; import ArticleHeader from "./PublicationHeader.svelte";
import { onMount } from "svelte"; import { onMount, onDestroy } from "svelte";
import { import {
getMatchingTags, getMatchingTags,
NDKRelaySetFromNDK, NDKRelaySetFromNDK,
@ -15,44 +15,109 @@
import { indexEventCache } from "$lib/utils/indexEventCache"; import { indexEventCache } from "$lib/utils/indexEventCache";
import { isValidNip05Address } from "$lib/utils/search_utility"; import { isValidNip05Address } from "$lib/utils/search_utility";
let { const props = $props<{
relays,
fallbackRelays,
searchQuery = "",
userRelays = [],
} = $props<{
relays: string[];
fallbackRelays: string[];
searchQuery?: string; searchQuery?: string;
userRelays?: string[]; onEventCountUpdate?: (counts: { displayed: number; total: number }) => void;
}>(); }>();
// Component state
let eventsInView: NDKEvent[] = $state([]); let eventsInView: NDKEvent[] = $state([]);
let loadingMore: boolean = $state(false); let loadingMore: boolean = $state(false);
let endOfFeed: boolean = $state(false); let endOfFeed: boolean = $state(false);
let relayStatuses = $state<Record<string, "pending" | "found" | "notfound">>( let relayStatuses = $state<Record<string, "pending" | "found" | "notfound">>({});
{},
);
let loading: boolean = $state(true); let loading: boolean = $state(true);
let hasInitialized = $state(false);
let fallbackTimeout: ReturnType<typeof setTimeout> | null = null;
// Relay management
let allRelays: string[] = $state([]);
let ndk = $derived($ndkInstance);
// Event management
let allIndexEvents: NDKEvent[] = $state([]);
let cutoffTimestamp: number = $derived( let cutoffTimestamp: number = $derived(
eventsInView?.at(eventsInView.length - 1)?.created_at ?? eventsInView?.at(eventsInView.length - 1)?.created_at ??
new Date().getTime(), new Date().getTime(),
); );
let allIndexEvents: NDKEvent[] = $state([]); // Initialize relays and fetch events
async function initializeAndFetch() {
if (!ndk) {
console.debug('[PublicationFeed] No NDK instance available');
return;
}
// Get relays from active stores
const inboxRelays = $activeInboxRelays;
const outboxRelays = $activeOutboxRelays;
const newRelays = [...inboxRelays, ...outboxRelays];
console.debug('[PublicationFeed] Available relays:', {
inboxCount: inboxRelays.length,
outboxCount: outboxRelays.length,
totalCount: newRelays.length,
relays: newRelays
});
if (newRelays.length === 0) {
console.debug('[PublicationFeed] No relays available, waiting...');
return;
}
// Update allRelays if different
const currentRelaysString = allRelays.sort().join(',');
const newRelaysString = newRelays.sort().join(',');
if (currentRelaysString !== newRelaysString) {
allRelays = newRelays;
console.debug('[PublicationFeed] Relays updated, fetching events');
await fetchAllIndexEventsFromRelays();
}
}
// Watch for relay store changes
$effect(() => {
const inboxRelays = $activeInboxRelays;
const outboxRelays = $activeOutboxRelays;
const newRelays = [...inboxRelays, ...outboxRelays];
if (newRelays.length > 0 && !hasInitialized) {
console.debug('[PublicationFeed] Relays available, initializing');
hasInitialized = true;
if (fallbackTimeout) {
clearTimeout(fallbackTimeout);
fallbackTimeout = null;
}
setTimeout(() => initializeAndFetch(), 0);
} else if (newRelays.length === 0 && !hasInitialized) {
console.debug('[PublicationFeed] No relays available, setting up fallback');
if (!fallbackTimeout) {
fallbackTimeout = setTimeout(() => {
console.debug('[PublicationFeed] Fallback timeout reached, retrying');
hasInitialized = true;
initializeAndFetch();
}, 3000);
}
}
});
async function fetchAllIndexEventsFromRelays() { async function fetchAllIndexEventsFromRelays() {
loading = true; console.debug('[PublicationFeed] fetchAllIndexEventsFromRelays called with relays:', {
const ndk = $ndkInstance; allRelaysCount: allRelays.length,
const communityRelays: string[] = relays; allRelays: allRelays
const userRelayList: string[] = userRelays || []; });
const fallback: string[] = fallbackRelays.filter(
(r: string) => !communityRelays.includes(r) && !userRelayList.includes(r), if (!ndk) {
); console.error('[PublicationFeed] No NDK instance available');
const allRelays = includeAllRelays loading = false;
? [...communityRelays, ...userRelayList, ...fallback] return;
: [...communityRelays, ...userRelayList]; }
if (allRelays.length === 0) {
console.debug('[PublicationFeed] No relays available for fetching');
loading = false;
return;
}
// Check cache first // Check cache first
const cachedEvents = indexEventCache.get(allRelays); const cachedEvents = indexEventCache.get(allRelays);
@ -67,6 +132,7 @@
return; return;
} }
loading = true;
relayStatuses = Object.fromEntries( relayStatuses = Object.fromEntries(
allRelays.map((r: string) => [r, "pending"]), allRelays.map((r: string) => [r, "pending"]),
); );
@ -75,11 +141,13 @@
// Helper to fetch from a single relay with timeout // Helper to fetch from a single relay with timeout
async function fetchFromRelay(relay: string): Promise<NDKEvent[]> { async function fetchFromRelay(relay: string): Promise<NDKEvent[]> {
try { try {
console.debug(`[PublicationFeed] Fetching from relay: ${relay}`);
const relaySet = NDKRelaySetFromNDK.fromRelayUrls([relay], ndk); const relaySet = NDKRelaySetFromNDK.fromRelayUrls([relay], ndk);
let eventSet = await ndk let eventSet = await ndk
.fetchEvents( .fetchEvents(
{ {
kinds: [indexKind], kinds: [indexKind],
limit: 1000, // Increased limit to get more events
}, },
{ {
groupable: false, groupable: false,
@ -88,29 +156,40 @@
}, },
relaySet, relaySet,
) )
.withTimeout(5000); .withTimeout(10000); // Increased timeout to 10 seconds
console.debug(`[PublicationFeed] Raw events from ${relay}:`, eventSet.size);
eventSet = filterValidIndexEvents(eventSet); eventSet = filterValidIndexEvents(eventSet);
console.debug(`[PublicationFeed] Valid events from ${relay}:`, eventSet.size);
relayStatuses = { ...relayStatuses, [relay]: "found" }; relayStatuses = { ...relayStatuses, [relay]: "found" };
return Array.from(eventSet); return Array.from(eventSet);
} catch (err) { } catch (err) {
console.error(`Error fetching from relay ${relay}:`, err); console.error(`[PublicationFeed] Error fetching from relay ${relay}:`, err);
relayStatuses = { ...relayStatuses, [relay]: "notfound" }; relayStatuses = { ...relayStatuses, [relay]: "notfound" };
return []; return [];
} }
} }
// Fetch from all relays in parallel, do not block on any single relay // Fetch from all relays in parallel, do not block on any single relay
console.debug(`[PublicationFeed] Starting fetch from ${allRelays.length} relays`);
const results = await Promise.allSettled(allRelays.map(fetchFromRelay)); const results = await Promise.allSettled(allRelays.map(fetchFromRelay));
for (const result of results) { for (const result of results) {
if (result.status === "fulfilled") { if (result.status === "fulfilled") {
allEvents = allEvents.concat(result.value); allEvents = allEvents.concat(result.value);
} }
} }
console.debug(`[PublicationFeed] Total events fetched:`, allEvents.length);
// Deduplicate by tagAddress // Deduplicate by tagAddress
const eventMap = new Map( const eventMap = new Map(
allEvents.map((event) => [event.tagAddress(), event]), allEvents.map((event) => [event.tagAddress(), event]),
); );
allIndexEvents = Array.from(eventMap.values()); allIndexEvents = Array.from(eventMap.values());
console.debug(`[PublicationFeed] Events after deduplication:`, allIndexEvents.length);
// Sort by created_at descending // Sort by created_at descending
allIndexEvents.sort((a, b) => b.created_at! - a.created_at!); allIndexEvents.sort((a, b) => b.created_at! - a.created_at!);
@ -121,12 +200,19 @@
eventsInView = allIndexEvents.slice(0, 30); eventsInView = allIndexEvents.slice(0, 30);
endOfFeed = allIndexEvents.length <= 30; endOfFeed = allIndexEvents.length <= 30;
loading = false; loading = false;
console.debug(`[PublicationFeed] Final state:`, {
totalEvents: allIndexEvents.length,
eventsInView: eventsInView.length,
endOfFeed,
loading
});
} }
// Function to filter events based on search query // Function to filter events based on search query
const filterEventsBySearch = (events: NDKEvent[]) => { const filterEventsBySearch = (events: NDKEvent[]) => {
if (!searchQuery) return events; if (!props.searchQuery) return events;
const query = searchQuery.toLowerCase(); const query = props.searchQuery.toLowerCase();
console.debug( console.debug(
"[PublicationFeed] Filtering events with query:", "[PublicationFeed] Filtering events with query:",
query, query,
@ -219,15 +305,25 @@
$effect(() => { $effect(() => {
console.debug( console.debug(
"[PublicationFeed] Search query effect triggered:", "[PublicationFeed] Search query effect triggered:",
searchQuery, props.searchQuery,
); );
debouncedSearch(searchQuery); debouncedSearch(props.searchQuery);
});
// Emit event count updates
$effect(() => {
if (props.onEventCountUpdate) {
props.onEventCountUpdate({
displayed: eventsInView.length,
total: allIndexEvents.length
});
}
}); });
async function loadMorePublications() { async function loadMorePublications() {
loadingMore = true; loadingMore = true;
const current = eventsInView.length; const current = eventsInView.length;
let source = searchQuery.trim() let source = props.searchQuery.trim()
? filterEventsBySearch(allIndexEvents) ? filterEventsBySearch(allIndexEvents)
: allIndexEvents; : allIndexEvents;
eventsInView = source.slice(0, current + 30); eventsInView = source.slice(0, current + 30);
@ -251,38 +347,27 @@
return `Index: ${indexStats.size} entries (${indexStats.totalEvents} events), Search: ${searchStats} entries`; return `Index: ${indexStats.size} entries (${indexStats.totalEvents} events), Search: ${searchStats} entries`;
} }
// Include all relays checkbox state // Cleanup function for fallback timeout
let includeAllRelays = $state(false); function cleanup() {
if (fallbackTimeout) {
// Watch for changes in include all relays setting clearTimeout(fallbackTimeout);
$effect(() => { fallbackTimeout = null;
console.log( }
`[PublicationFeed] Include all relays setting changed to: ${includeAllRelays}`, }
);
// Clear cache when relay configuration changes
indexEventCache.clear();
searchCache.clear();
// Refetch events with new relay configuration // Cleanup on component destruction
fetchAllIndexEventsFromRelays(); onDestroy(() => {
cleanup();
}); });
onMount(async () => { onMount(async () => {
await fetchAllIndexEventsFromRelays(); console.debug('[PublicationFeed] onMount called');
// The effect will handle fetching when relays become available
}); });
</script> </script>
<div class="flex flex-col space-y-4"> <div class="flex flex-col space-y-4">
<!-- Include all relays checkbox -->
<div class="flex items-center justify-center">
<Checkbox bind:checked={includeAllRelays} class="mr-2" />
<label
for="include-all-relays"
class="text-sm text-gray-700 dark:text-gray-300"
>
Include all relays (slower but more comprehensive search)
</label>
</div>
<div <div
class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 w-full" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 w-full"

11
src/lib/components/publications/PublicationHeader.svelte

@ -2,15 +2,20 @@
import { ndkInstance } from "$lib/ndk"; import { ndkInstance } from "$lib/ndk";
import { naddrEncode } from "$lib/utils"; import { naddrEncode } from "$lib/utils";
import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { standardRelays } from "../../consts"; import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
import { communityRelays } from "../../consts";
import { Card, Img } from "flowbite-svelte"; import { Card, Img } from "flowbite-svelte";
import CardActions from "$components/util/CardActions.svelte"; import CardActions from "$components/util/CardActions.svelte";
import { userBadge } from "$lib/snippets/UserSnippets.svelte"; import { userBadge } from "$lib/snippets/UserSnippets.svelte";
const { event } = $props<{ event: NDKEvent }>(); const { event } = $props<{ event: NDKEvent }>();
function getRelayUrls(): string[] {
return $activeInboxRelays;
}
const relays = $derived.by(() => { const relays = $derived.by(() => {
return $ndkInstance.activeUser?.relayUrls ?? standardRelays; return getRelayUrls();
}); });
const href = $derived.by(() => { const href = $derived.by(() => {
@ -33,8 +38,6 @@
let authorPubkey: string = $derived( let authorPubkey: string = $derived(
event.getMatchingTags("p")[0]?.[1] ?? null, event.getMatchingTags("p")[0]?.[1] ?? null,
); );
console.log("PublicationHeader event:", event);
</script> </script>
{#if title != null && href != null} {#if title != null && href != null}

10
src/lib/components/publications/PublicationSection.svelte

@ -10,6 +10,7 @@
import type { Asciidoctor, Document } from "asciidoctor"; import type { Asciidoctor, Document } from "asciidoctor";
import { getMatchingTags } from "$lib/utils/nostrUtils"; import { getMatchingTags } from "$lib/utils/nostrUtils";
import type { SveltePublicationTree } from "./svelte_publication_tree.svelte"; import type { SveltePublicationTree } from "./svelte_publication_tree.svelte";
import { postProcessAdvancedAsciidoctorHtml } from "$lib/utils/markup/advancedAsciidoctorPostProcessor";
let { let {
address, address,
@ -46,9 +47,12 @@
async () => (await leafEvent)?.getMatchingTags("title")[0]?.[1], async () => (await leafEvent)?.getMatchingTags("title")[0]?.[1],
); );
let leafContent: Promise<string | Document> = $derived.by(async () => let leafContent: Promise<string | Document> = $derived.by(async () => {
asciidoctor.convert((await leafEvent)?.content ?? ""), const content = (await leafEvent)?.content ?? "";
); const converted = asciidoctor.convert(content);
const processed = await postProcessAdvancedAsciidoctorHtml(converted.toString());
return processed;
});
let previousLeafEvent: NDKEvent | null = $derived.by(() => { let previousLeafEvent: NDKEvent | null = $derived.by(() => {
let index: number; let index: number;

16
src/lib/components/util/CardActions.svelte

@ -8,10 +8,9 @@
import CopyToClipboard from "$components/util/CopyToClipboard.svelte"; import CopyToClipboard from "$components/util/CopyToClipboard.svelte";
import { userBadge } from "$lib/snippets/UserSnippets.svelte"; import { userBadge } from "$lib/snippets/UserSnippets.svelte";
import { neventEncode, naddrEncode } from "$lib/utils"; import { neventEncode, naddrEncode } from "$lib/utils";
import { standardRelays, fallbackRelays, FeedType } from "$lib/consts"; import { communityRelays, secondaryRelays, FeedType } from "$lib/consts";
import { ndkSignedIn, inboxRelays } from "$lib/ndk"; import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
import { feedType } from "$lib/stores"; import { userStore } from "$lib/stores/userStore";
import { userStore } from "$lib/stores/userStore";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import type { NDKEvent } from "$lib/utils/nostrUtils"; import type { NDKEvent } from "$lib/utils/nostrUtils";
@ -63,19 +62,16 @@
/** /**
* Selects the appropriate relay set based on user state and feed type * Selects the appropriate relay set based on user state and feed type
* - Uses user's inbox relays when signed in and viewing personal feed * - Uses active inbox relays from the new relay management system
* - Falls back to standard relays for anonymous users or standard feed * - Falls back to active inbox relays for anonymous users (which include community relays)
*/ */
let activeRelays = $derived( let activeRelays = $derived(
(() => { (() => {
const isUserFeed = user.signedIn && $feedType === FeedType.UserRelays; const relays = user.signedIn ? $activeInboxRelays : $activeInboxRelays;
const relays = isUserFeed ? user.relays.inbox : standardRelays;
console.debug("[CardActions] Selected relays:", { console.debug("[CardActions] Selected relays:", {
eventId: event.id, eventId: event.id,
isSignedIn: user.signedIn, isSignedIn: user.signedIn,
feedType: $feedType,
isUserFeed,
relayCount: relays.length, relayCount: relays.length,
relayUrls: relays, relayUrls: relays,
}); });

8
src/lib/components/util/ContainingIndexes.svelte

@ -5,7 +5,7 @@
import { findContainingIndexEvents } from "$lib/utils/event_search"; import { findContainingIndexEvents } from "$lib/utils/event_search";
import { getMatchingTags } from "$lib/utils/nostrUtils"; import { getMatchingTags } from "$lib/utils/nostrUtils";
import { naddrEncode } from "$lib/utils"; import { naddrEncode } from "$lib/utils";
import { standardRelays } from "$lib/consts"; import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
let { event } = $props<{ let { event } = $props<{
event: NDKEvent; event: NDKEvent;
@ -51,7 +51,7 @@
} else { } else {
// Fallback to naddr // Fallback to naddr
try { try {
const naddr = naddrEncode(indexEvent, standardRelays); const naddr = naddrEncode(indexEvent, $activeInboxRelays);
goto(`/publication?id=${encodeURIComponent(naddr)}`); goto(`/publication?id=${encodeURIComponent(naddr)}`);
} catch (err) { } catch (err) {
console.error("[ContainingIndexes] Error creating naddr:", err); console.error("[ContainingIndexes] Error creating naddr:", err);
@ -59,6 +59,10 @@
} }
} }
function getNaddrUrl(event: NDKEvent): string {
return naddrEncode(event, $activeInboxRelays);
}
$effect(() => { $effect(() => {
// Only reload if the event ID has actually changed // Only reload if the event ID has actually changed
if (event.id !== lastEventId) { if (event.id !== lastEventId) {

165
src/lib/components/util/Profile.svelte

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import CopyToClipboard from "$components/util/CopyToClipboard.svelte"; import CopyToClipboard from "$components/util/CopyToClipboard.svelte";
import { logoutUser } from "$lib/stores/userStore"; import NetworkStatus from "$components/NetworkStatus.svelte";
import { logoutUser, userStore } from "$lib/stores/userStore";
import { ndkInstance } from "$lib/ndk"; import { ndkInstance } from "$lib/ndk";
import { import {
ArrowRightToBracketOutline, ArrowRightToBracketOutline,
@ -14,22 +15,29 @@
let { pubkey, isNav = false } = $props(); let { pubkey, isNav = false } = $props();
let profile = $state<NDKUserProfile | null>(null); // Use profile data from userStore instead of fetching separately
let pfp = $derived(profile?.image); let userState = $derived($userStore);
let profile = $derived(userState.profile);
let pfp = $derived(profile?.picture);
let username = $derived(profile?.name); let username = $derived(profile?.name);
let tag = $derived(profile?.name); let tag = $derived(profile?.name);
let npub = $state<string | undefined>(undefined); let npub = $derived(userState.npub);
// Fallback to fetching profile if not available in userStore
$effect(() => { $effect(() => {
const ndk = get(ndkInstance); if (!profile && pubkey) {
if (!ndk) return; const ndk = get(ndkInstance);
if (!ndk) return;
const user = ndk.getUser({ pubkey: pubkey ?? undefined }); const user = ndk.getUser({ pubkey: pubkey ?? undefined });
npub = user.npub;
user.fetchProfile().then((userProfile: NDKUserProfile | null) => {
user.fetchProfile().then((userProfile: NDKUserProfile | null) => { if (userProfile && !profile) {
profile = userProfile; // Only update if we don't already have profile data
}); profile = userProfile;
}
});
}
}); });
async function handleSignOutClick() { async function handleSignOutClick() {
@ -43,78 +51,85 @@
} }
} }
function shortenNpub(long: string | undefined) { function shortenNpub(long: string | null | undefined) {
if (!long) return ""; if (!long) return "";
return long.slice(0, 8) + "…" + long.slice(-4); return long.slice(0, 8) + "…" + long.slice(-4);
} }
</script> </script>
<div class="relative"> <div class="relative">
{#if profile} <div class="group">
<div class="group"> <button
class="h-6 w-6 rounded-full p-0 border-0 bg-transparent cursor-pointer"
id="profile-avatar"
type="button"
aria-label="Open profile menu"
>
<Avatar <Avatar
rounded rounded
class="h-6 w-6 cursor-pointer" class="h-6 w-6 cursor-pointer"
src={pfp} src={pfp}
alt={username} alt={username || "User"}
id="profile-avatar"
/> />
{#key username || tag} </button>
<Popover <Popover
placement="bottom" placement="bottom"
triggeredBy="#profile-avatar" triggeredBy="#profile-avatar"
class="popover-leather w-[180px]" class="popover-leather w-[180px]"
trigger="hover" trigger="click"
> >
<div class="flex flex-row justify-between space-x-4"> <div class="flex flex-row justify-between space-x-4">
<div class="flex flex-col"> <div class="flex flex-col">
{#if username} {#if username}
<h3 class="text-lg font-bold">{username}</h3> <h3 class="text-lg font-bold">{username}</h3>
{#if isNav}<h4 class="text-base">@{tag}</h4>{/if} {#if isNav}<h4 class="text-base">@{tag}</h4>{/if}
{/if} {:else}
<ul class="space-y-2 mt-2"> <h3 class="text-lg font-bold">Loading...</h3>
<li> {/if}
<CopyToClipboard <ul class="space-y-2 mt-2">
displayText={shortenNpub(npub)} <li>
copyText={npub} <CopyToClipboard
/> displayText={shortenNpub(npub) || "Loading..."}
</li> copyText={npub || ""}
<li> />
<button </li>
class="hover:text-primary-400 dark:hover:text-primary-500 text-nowrap mt-3 m-0 text-left" <li>
onclick={handleViewProfile} <button
> class="hover:text-primary-400 dark:hover:text-primary-500 text-nowrap mt-3 m-0 text-left"
<UserOutline onclick={handleViewProfile}
class="mr-1 !h-6 !w-6 inline !fill-none dark:!fill-none" >
/><span class="underline">View profile</span> <UserOutline
</button> class="mr-1 !h-6 !w-6 inline !fill-none dark:!fill-none"
</li> /><span class="underline">View profile</span>
{#if isNav} </button>
<li> </li>
<button <li>
id="sign-out-button" <NetworkStatus />
class="btn-leather text-nowrap mt-3 flex self-stretch align-middle hover:text-primary-400 dark:hover:text-primary-500" </li>
onclick={handleSignOutClick} {#if isNav}
> <li>
<ArrowRightToBracketOutline <button
class="mr-1 !h-6 !w-6 inline !fill-none dark:!fill-none" id="sign-out-button"
/> Sign out class="btn-leather text-nowrap mt-3 flex self-stretch align-middle hover:text-primary-400 dark:hover:text-primary-500"
</button> onclick={handleSignOutClick}
</li> >
{:else} <ArrowRightToBracketOutline
<!-- li> class="mr-1 !h-6 !w-6 inline !fill-none dark:!fill-none"
<button /> Sign out
class='btn-leather text-nowrap mt-3 flex self-stretch align-middle hover:text-primary-400 dark:hover:text-primary-500' </button>
> </li>
<FileSearchOutline class='mr-1 !h-6 inline !fill-none dark:!fill-none' /> More content {:else}
</button> <!-- li>
</li --> <button
{/if} class='btn-leather text-nowrap mt-3 flex self-stretch align-middle hover:text-primary-400 dark:hover:text-primary-500'
</ul> >
</div> <FileSearchOutline class='mr-1 !h-6 inline !fill-none dark:!fill-none' /> More content
</div> </button>
</Popover> </li -->
{/key} {/if}
</div> </ul>
{/if} </div>
</div>
</Popover>
</div>
</div> </div>

5
src/lib/components/util/ViewPublicationLink.svelte

@ -3,7 +3,8 @@
import { getMatchingTags } from "$lib/utils/nostrUtils"; import { getMatchingTags } from "$lib/utils/nostrUtils";
import { naddrEncode } from "$lib/utils"; import { naddrEncode } from "$lib/utils";
import { getEventType } from "$lib/utils/mime"; import { getEventType } from "$lib/utils/mime";
import { standardRelays } from "$lib/consts"; import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
import { communityRelays } from "$lib/consts";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
let { event, className = "" } = $props<{ let { event, className = "" } = $props<{
@ -25,7 +26,7 @@
return null; return null;
} }
try { try {
return naddrEncode(event, standardRelays); return naddrEncode(event, $activeInboxRelays);
} catch { } catch {
return null; return null;
} }

55
src/lib/consts.ts

@ -1,45 +1,48 @@
export const wikiKind = 30818; export const wikiKind = 30818;
export const indexKind = 30040; export const indexKind = 30040;
export const zettelKinds = [30041, 30818]; export const zettelKinds = [30041, 30818];
export const communityRelay = "wss://theforest.nostr1.com";
export const profileRelays = [ export const communityRelays = [
"wss://theforest.nostr1.com",
//"wss://theforest.gitcitadel.eu"
];
export const searchRelays = [
"wss://profiles.nostr1.com", "wss://profiles.nostr1.com",
"wss://aggr.nostr.land", "wss://aggr.nostr.land",
"wss://relay.noswhere.com", "wss://relay.noswhere.com",
]; "wss://nostr.wine",
export const standardRelays = [
"wss://thecitadel.nostr1.com",
"wss://theforest.nostr1.com",
"wss://profiles.nostr1.com",
// Removed gitcitadel.nostr1.com as it's causing connection issues
//'wss://thecitadel.gitcitadel.eu',
//'wss://theforest.gitcitadel.eu',
]; ];
// Non-auth relays for anonymous users export const secondaryRelays = [
export const anonymousRelays = [
"wss://thecitadel.nostr1.com",
"wss://theforest.nostr1.com", "wss://theforest.nostr1.com",
"wss://profiles.nostr1.com", //"wss://theforest.gitcitadel.eu"
"wss://freelay.sovbit.host", "wss://thecitadel.nostr1.com",
]; //"wss://thecitadel.gitcitadel.eu",
export const fallbackRelays = [
"wss://purplepag.es",
"wss://indexer.coracle.social",
"wss://relay.noswhere.com",
"wss://aggr.nostr.land",
"wss://nostr.land", "wss://nostr.land",
"wss://nostr.wine", "wss://nostr.wine",
"wss://nostr.sovbit.host", "wss://nostr.sovbit.host",
"wss://freelay.sovbit.host",
"wss://nostr21.com", "wss://nostr21.com",
"wss://greensoul.space", ];
"wss://relay.damus.io",
"wss://relay.nostr.band", export const anonymousRelays = [
"wss://freelay.sovbit.host",
"wss://thecitadel.nostr1.com"
];
export const lowbandwidthRelays = [
"wss://theforest.nostr1.com",
"wss://thecitadel.nostr1.com",
"wss://aggr.nostr.land"
];
export const localRelays = [
"wss://localhost:8080",
"wss://localhost:4869"
]; ];
export enum FeedType { export enum FeedType {
StandardRelays = "standard", CommunityRelays = "standard",
UserRelays = "user", UserRelays = "user",
} }

7
src/lib/navigator/EventNetwork/utils/networkBuilder.ts

@ -8,8 +8,9 @@
import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { NDKEvent } from "@nostr-dev-kit/ndk";
import type { NetworkNode, NetworkLink, GraphData, GraphState } from "../types"; import type { NetworkNode, NetworkLink, GraphData, GraphState } from "../types";
import { nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
import { standardRelays } from "$lib/consts"; import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
import { getMatchingTags } from "$lib/utils/nostrUtils"; import { getMatchingTags } from "$lib/utils/nostrUtils";
import { get } from "svelte/store";
// Configuration // Configuration
const DEBUG = false; // Set to true to enable debug logging const DEBUG = false; // Set to true to enable debug logging
@ -71,13 +72,13 @@ export function createNetworkNode(
pubkey: event.pubkey, pubkey: event.pubkey,
identifier: dTag, identifier: dTag,
kind: event.kind, kind: event.kind,
relays: standardRelays, relays: [...get(activeInboxRelays), ...get(activeOutboxRelays)],
}); });
// Create nevent (NIP-19 event reference) for the event // Create nevent (NIP-19 event reference) for the event
node.nevent = nip19.neventEncode({ node.nevent = nip19.neventEncode({
id: event.id, id: event.id,
relays: standardRelays, relays: [...get(activeInboxRelays), ...get(activeOutboxRelays)],
kind: event.kind, kind: event.kind,
}); });
} catch (error) { } catch (error) {

415
src/lib/ndk.ts

@ -8,15 +8,29 @@ import NDK, {
} from "@nostr-dev-kit/ndk"; } from "@nostr-dev-kit/ndk";
import { get, writable, type Writable } from "svelte/store"; import { get, writable, type Writable } from "svelte/store";
import { import {
fallbackRelays, secondaryRelays,
FeedType, FeedType,
loginStorageKey, loginStorageKey,
standardRelays, communityRelays,
anonymousRelays, anonymousRelays,
searchRelays,
} from "./consts"; } from "./consts";
import { feedType } from "./stores"; import {
buildCompleteRelaySet,
testRelayConnection,
discoverLocalRelays,
getUserLocalRelays,
getUserBlockedRelays,
getUserOutboxRelays,
deduplicateRelayUrls,
} from "./utils/relay_management";
// Re-export testRelayConnection for components that need it
export { testRelayConnection };
import { startNetworkMonitoring, NetworkCondition } from "./utils/network_detection";
import { userStore } from "./stores/userStore"; import { userStore } from "./stores/userStore";
import { userPubkey } from "$lib/stores/authStore.Svelte"; import { userPubkey } from "$lib/stores/authStore.Svelte";
import { startNetworkStatusMonitoring, stopNetworkStatusMonitoring } from "./stores/networkStore";
export const ndkInstance: Writable<NDK> = writable(); export const ndkInstance: Writable<NDK> = writable();
export const ndkSignedIn = writable(false); export const ndkSignedIn = writable(false);
@ -24,6 +38,10 @@ export const activePubkey = writable<string | null>(null);
export const inboxRelays = writable<string[]>([]); export const inboxRelays = writable<string[]>([]);
export const outboxRelays = writable<string[]>([]); export const outboxRelays = writable<string[]>([]);
// New relay management stores
export const activeInboxRelays = writable<string[]>([]);
export const activeOutboxRelays = writable<string[]>([]);
/** /**
* Custom authentication policy that handles NIP-42 authentication manually * Custom authentication policy that handles NIP-42 authentication manually
* when the default NDK authentication fails * when the default NDK authentication fails
@ -207,83 +225,7 @@ export function checkWebSocketSupport(): void {
} }
} }
/**
* Tests connection to a relay and returns connection status
* @param relayUrl The relay URL to test
* @param ndk The NDK instance
* @returns Promise that resolves to connection status
*/
export async function testRelayConnection(
relayUrl: string,
ndk: NDK,
): Promise<{
connected: boolean;
requiresAuth: boolean;
error?: string;
actualUrl?: string;
}> {
return new Promise((resolve) => {
console.debug(`[NDK.ts] Testing connection to: ${relayUrl}`);
// Ensure the URL is using wss:// protocol
const secureUrl = ensureSecureWebSocket(relayUrl);
const relay = new NDKRelay(secureUrl, undefined, new NDK());
let authRequired = false;
let connected = false;
let error: string | undefined;
let actualUrl: string | undefined;
const timeout = setTimeout(() => {
relay.disconnect();
resolve({
connected: false,
requiresAuth: authRequired,
error: "Connection timeout",
actualUrl,
});
}, 5000);
relay.on("connect", () => {
console.debug(`[NDK.ts] Connected to ${secureUrl}`);
connected = true;
actualUrl = secureUrl;
clearTimeout(timeout);
relay.disconnect();
resolve({
connected: true,
requiresAuth: authRequired,
error,
actualUrl,
});
});
relay.on("notice", (message: string) => {
if (message.includes("auth-required")) {
authRequired = true;
console.debug(`[NDK.ts] ${secureUrl} requires authentication`);
}
});
relay.on("disconnect", () => {
if (!connected) {
error = "Connection failed";
console.error(`[NDK.ts] Failed to connect to ${secureUrl}`);
clearTimeout(timeout);
resolve({
connected: false,
requiresAuth: authRequired,
error,
actualUrl,
});
}
});
// Log the actual WebSocket URL being used
console.debug(`[NDK.ts] Attempting connection to: ${secureUrl}`);
relay.connect();
});
}
/** /**
* Gets the user's pubkey from local storage, if it exists. * Gets the user's pubkey from local storage, if it exists.
@ -433,98 +375,180 @@ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay {
return relay; return relay;
} }
export function getActiveRelays(ndk: NDK): NDKRelaySet {
/**
* Gets the active relay set for the current user
* @param ndk NDK instance
* @returns Promise that resolves to object with inbox and outbox relay arrays
*/
export async function getActiveRelaySet(ndk: NDK): Promise<{ inboxRelays: string[]; outboxRelays: string[] }> {
const user = get(userStore); const user = get(userStore);
if (user.signedIn && user.ndkUser) {
return await buildCompleteRelaySet(ndk, user.ndkUser);
} else {
return await buildCompleteRelaySet(ndk, null);
}
}
// Filter out problematic relays that are known to cause connection issues /**
const filterProblematicRelays = (relays: string[]) => { * Updates the active relay stores and NDK pool with new relay URLs
return relays.filter((relay) => { * @param ndk NDK instance
// Filter out gitcitadel.nostr1.com which is causing connection issues */
if (relay.includes("gitcitadel.nostr1.com")) { export async function updateActiveRelayStores(ndk: NDK): Promise<void> {
console.warn(`[NDK.ts] Filtering out problematic relay: ${relay}`); try {
return false; // Get the active relay set from the relay management system
const relaySet = await getActiveRelaySet(ndk);
// Update the stores with the new relay configuration
activeInboxRelays.set(relaySet.inboxRelays);
activeOutboxRelays.set(relaySet.outboxRelays);
// Add relays to NDK pool (deduplicated)
const allRelayUrls = deduplicateRelayUrls([...relaySet.inboxRelays, ...relaySet.outboxRelays]);
for (const url of allRelayUrls) {
try {
const relay = createRelayWithAuth(url, ndk);
ndk.pool?.addRelay(relay);
} catch (error) {
// Silently ignore relay addition failures
} }
return true; }
}); } catch (error) {
}; // Silently ignore relay store update errors
}
}
return get(feedType) === FeedType.UserRelays && user.signedIn /**
? new NDKRelaySet( * Logs the current relay configuration to console
new Set( */
filterProblematicRelays(user.relays.inbox).map( export function logCurrentRelayConfiguration(): void {
(relay) => const inboxRelays = get(activeInboxRelays);
new NDKRelay(relay, NDKRelayAuthPolicies.signIn({ ndk }), ndk), const outboxRelays = get(activeOutboxRelays);
),
), console.log('🔌 Current Relay Configuration:');
ndk, console.log('📥 Inbox Relays:', inboxRelays);
) console.log('📤 Outbox Relays:', outboxRelays);
: new NDKRelaySet( console.log(`📊 Total: ${inboxRelays.length} inbox, ${outboxRelays.length} outbox`);
new Set(
filterProblematicRelays(standardRelays).map(
(relay) =>
new NDKRelay(relay, NDKRelayAuthPolicies.signIn({ ndk }), ndk),
),
),
ndk,
);
} }
/** /**
* Initializes an instance of NDK, and connects it to the logged-in user's preferred relay set * Updates relay stores when user state changes
* (if available), or to Alexandria's standard relay set. * @param ndk NDK instance
* @returns The initialized NDK instance.
*/ */
export function initNdk(): NDK { export async function refreshRelayStores(ndk: NDK): Promise<void> {
const startingPubkey = getPersistedLogin(); console.debug('[NDK.ts] Refreshing relay stores due to user state change');
const [startingInboxes, _] = await updateActiveRelayStores(ndk);
startingPubkey != null }
? getPersistedRelays(new NDKUser({ pubkey: startingPubkey }))
: [null, null]; /**
* Updates relay stores when network condition changes
* @param ndk NDK instance
*/
export async function refreshRelayStoresOnNetworkChange(ndk: NDK): Promise<void> {
console.debug('[NDK.ts] Refreshing relay stores due to network condition change');
await updateActiveRelayStores(ndk);
}
/**
* Starts network monitoring for relay optimization
* @param ndk NDK instance
*/
export function startNetworkMonitoringForRelays(ndk: NDK): void {
// Use centralized network monitoring instead of separate monitoring
startNetworkStatusMonitoring();
}
// Ensure all relay URLs use secure WebSocket protocol /**
const secureRelayUrls = ( * Creates NDKRelaySet from relay URLs with proper authentication
startingInboxes != null * @param relayUrls Array of relay URLs
? Array.from(startingInboxes.values()) * @param ndk NDK instance
: anonymousRelays * @returns NDKRelaySet
).map(ensureSecureWebSocket); */
function createRelaySetFromUrls(relayUrls: string[], ndk: NDK): NDKRelaySet {
const relays = relayUrls.map(url =>
new NDKRelay(url, NDKRelayAuthPolicies.signIn({ ndk }), ndk)
);
return new NDKRelaySet(new Set(relays), ndk);
}
console.debug("[NDK.ts] Initializing NDK with relay URLs:", secureRelayUrls); /**
* Gets the active relay set as NDKRelaySet for use in queries
* @param ndk NDK instance
* @param useInbox Whether to use inbox relays (true) or outbox relays (false)
* @returns Promise that resolves to NDKRelaySet
*/
export async function getActiveRelaySetAsNDKRelaySet(
ndk: NDK,
useInbox: boolean = true
): Promise<NDKRelaySet> {
const relaySet = await getActiveRelaySet(ndk);
const urls = useInbox ? relaySet.inboxRelays : relaySet.outboxRelays;
return createRelaySetFromUrls(urls, ndk);
}
/**
* Initializes an instance of NDK with the new relay management system
* @returns The initialized NDK instance
*/
export function initNdk(): NDK {
console.debug("[NDK.ts] Initializing NDK with new relay management system");
const ndk = new NDK({ const ndk = new NDK({
autoConnectUserRelays: true, autoConnectUserRelays: false, // We'll manage relays manually
enableOutboxModel: true, enableOutboxModel: true,
explicitRelayUrls: secureRelayUrls,
}); });
// Set up custom authentication policy // Set up custom authentication policy
ndk.relayAuthDefaultPolicy = NDKRelayAuthPolicies.signIn({ ndk }); ndk.relayAuthDefaultPolicy = NDKRelayAuthPolicies.signIn({ ndk });
// Connect with better error handling // Connect with better error handling and reduced retry attempts
ndk let retryCount = 0;
.connect() const maxRetries = 2;
.then(() => {
const attemptConnection = async () => {
try {
await ndk.connect();
console.debug("[NDK.ts] NDK connected successfully"); console.debug("[NDK.ts] NDK connected successfully");
}) // Update relay stores after connection
.catch((error) => { await updateActiveRelayStores(ndk);
console.error("[NDK.ts] Failed to connect NDK:", error); // Start network monitoring for relay optimization
// Try to reconnect after a delay startNetworkMonitoringForRelays(ndk);
setTimeout(() => { } catch (error) {
console.debug("[NDK.ts] Attempting to reconnect..."); console.warn("[NDK.ts] Failed to connect NDK:", error);
ndk.connect().catch((retryError) => {
console.error("[NDK.ts] Reconnection failed:", retryError); // Only retry a limited number of times
}); if (retryCount < maxRetries) {
}, 5000); retryCount++;
}); console.debug(`[NDK.ts] Attempting to reconnect (${retryCount}/${maxRetries})...`);
setTimeout(attemptConnection, 3000);
} else {
console.warn("[NDK.ts] Max retries reached, continuing with limited functionality");
// Still try to update relay stores even if connection failed
try {
await updateActiveRelayStores(ndk);
startNetworkMonitoringForRelays(ndk);
} catch (storeError) {
console.warn("[NDK.ts] Failed to update relay stores:", storeError);
}
}
}
};
attemptConnection();
return ndk; return ndk;
} }
/** /**
* Signs in with a NIP-07 browser extension, and determines the user's preferred inbox and outbox * Signs in with a NIP-07 browser extension using the new relay management system
* relays. * @returns The user's profile, if it is available
* @returns The user's profile, if it is available. * @throws If sign-in fails
* @throws If sign-in fails. This may because there is no accessible NIP-07 extension, or because
* NDK is unable to fetch the user's profile or relay lists.
*/ */
export async function loginWithExtension( export async function loginWithExtension(
pubkey?: string, pubkey?: string,
@ -542,23 +566,10 @@ export async function loginWithExtension(
activePubkey.set(signerUser.pubkey); activePubkey.set(signerUser.pubkey);
userPubkey.set(signerUser.pubkey); userPubkey.set(signerUser.pubkey);
const [persistedInboxes, persistedOutboxes] =
getPersistedRelays(signerUser);
for (const relay of persistedInboxes) {
ndk.addExplicitRelay(relay);
}
const user = ndk.getUser({ pubkey: signerUser.pubkey }); const user = ndk.getUser({ pubkey: signerUser.pubkey });
const [inboxes, outboxes] = await getUserPreferredRelays(ndk, user);
// Update relay stores with the new system
inboxRelays.set( await updateActiveRelayStores(ndk);
Array.from(inboxes ?? persistedInboxes).map((relay) => relay.url),
);
outboxRelays.set(
Array.from(outboxes ?? persistedOutboxes).map((relay) => relay.url),
);
persistRelays(signerUser, inboxes, outboxes);
ndk.signer = signer; ndk.signer = signer;
ndk.activeUser = user; ndk.activeUser = user;
@ -582,73 +593,17 @@ export function logout(user: NDKUser): void {
activePubkey.set(null); activePubkey.set(null);
userPubkey.set(null); userPubkey.set(null);
ndkSignedIn.set(false); ndkSignedIn.set(false);
ndkInstance.set(initNdk()); // Re-initialize with anonymous instance
// Clear relay stores
activeInboxRelays.set([]);
activeOutboxRelays.set([]);
// Stop network monitoring
stopNetworkStatusMonitoring();
// Re-initialize with anonymous instance
const newNdk = initNdk();
ndkInstance.set(newNdk);
} }
/**
* Fetches the user's NIP-65 relay list, if one can be found, and returns the inbox and outbox
* relay sets.
* @returns A tuple of relay sets of the form `[inboxRelays, outboxRelays]`.
*/
export async function getUserPreferredRelays(
ndk: NDK,
user: NDKUser,
fallbacks: readonly string[] = fallbackRelays,
): Promise<[Set<NDKRelay>, Set<NDKRelay>]> {
const relayList = await ndk.fetchEvent(
{
kinds: [10002],
authors: [user.pubkey],
},
{
groupable: false,
skipVerification: false,
skipValidation: false,
},
NDKRelaySet.fromRelayUrls(fallbacks, ndk),
);
const inboxRelays = new Set<NDKRelay>();
const outboxRelays = new Set<NDKRelay>();
// Filter out problematic relays
const filterProblematicRelay = (url: string): boolean => {
if (url.includes("gitcitadel.nostr1.com")) {
console.warn(
`[NDK.ts] Filtering out problematic relay from user preferences: ${url}`,
);
return false;
}
return true;
};
if (relayList == null) {
const relayMap = await window.nostr?.getRelays?.();
Object.entries(relayMap ?? {}).forEach(([url, relayType]) => {
if (filterProblematicRelay(url)) {
const relay = createRelayWithAuth(url, ndk);
if (relayType.read) inboxRelays.add(relay);
if (relayType.write) outboxRelays.add(relay);
}
});
} else {
relayList.tags.forEach((tag) => {
if (filterProblematicRelay(tag[1])) {
switch (tag[0]) {
case "r":
inboxRelays.add(createRelayWithAuth(tag[1], ndk));
break;
case "w":
outboxRelays.add(createRelayWithAuth(tag[1], ndk));
break;
default:
inboxRelays.add(createRelayWithAuth(tag[1], ndk));
outboxRelays.add(createRelayWithAuth(tag[1], ndk));
break;
}
}
});
}
return [inboxRelays, outboxRelays];
}

10
src/lib/stores.ts

@ -1,11 +1,11 @@
import { readable, writable } from "svelte/store"; import { writable } from "svelte/store";
import { FeedType } from "./consts.ts";
export let idList = writable<string[]>([]); // The old feedType store is no longer needed since we use the new relay management system
// All relay selection is now handled by the activeInboxRelays and activeOutboxRelays stores in ndk.ts
export let alexandriaKinds = readable<number[]>([30040, 30041, 30818]); export let idList = writable<string[]>([]);
export let feedType = writable<FeedType>(FeedType.StandardRelays); export let alexandriaKinds = writable<number[]>([30040, 30041, 30818]);
export interface PublicationLayoutVisibility { export interface PublicationLayoutVisibility {
toc: boolean; toc: boolean;

55
src/lib/stores/networkStore.ts

@ -0,0 +1,55 @@
import { writable, type Writable } from 'svelte/store';
import { detectNetworkCondition, NetworkCondition, startNetworkMonitoring } from '$lib/utils/network_detection';
// Network status store
export const networkCondition = writable<NetworkCondition>(NetworkCondition.ONLINE);
export const isNetworkChecking = writable<boolean>(false);
// Network monitoring state
let stopNetworkMonitoring: (() => void) | null = null;
/**
* Starts network monitoring if not already running
*/
export function startNetworkStatusMonitoring(): void {
if (stopNetworkMonitoring) {
return; // Already monitoring
}
console.debug('[networkStore.ts] Starting network status monitoring');
stopNetworkMonitoring = startNetworkMonitoring(
(condition: NetworkCondition) => {
console.debug(`[networkStore.ts] Network condition changed to: ${condition}`);
networkCondition.set(condition);
},
60000 // Check every 60 seconds to reduce spam
);
}
/**
* Stops network monitoring
*/
export function stopNetworkStatusMonitoring(): void {
if (stopNetworkMonitoring) {
console.debug('[networkStore.ts] Stopping network status monitoring');
stopNetworkMonitoring();
stopNetworkMonitoring = null;
}
}
/**
* Manually check network status (for immediate updates)
*/
export async function checkNetworkStatus(): Promise<void> {
try {
isNetworkChecking.set(true);
const condition = await detectNetworkCondition();
networkCondition.set(condition);
} catch (error) {
console.warn('[networkStore.ts] Failed to check network status:', error);
networkCondition.set(NetworkCondition.OFFLINE);
} finally {
isNetworkChecking.set(false);
}
}

4
src/lib/stores/relayStore.ts

@ -1,4 +0,0 @@
import { writable } from "svelte/store";
// Initialize with empty array, will be populated from user preferences
export const userRelays = writable<string[]>([]);

6
src/lib/stores/userStore.ts

@ -8,8 +8,8 @@ import {
NDKRelay, NDKRelay,
} from "@nostr-dev-kit/ndk"; } from "@nostr-dev-kit/ndk";
import { getUserMetadata } from "$lib/utils/nostrUtils"; import { getUserMetadata } from "$lib/utils/nostrUtils";
import { ndkInstance } from "$lib/ndk"; import { ndkInstance, activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
import { loginStorageKey, fallbackRelays } from "$lib/consts"; import { loginStorageKey } from "$lib/consts";
import { nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
export interface UserState { export interface UserState {
@ -70,7 +70,7 @@ function getPersistedRelays(user: NDKUser): [Set<string>, Set<string>] {
async function getUserPreferredRelays( async function getUserPreferredRelays(
ndk: any, ndk: any,
user: NDKUser, user: NDKUser,
fallbacks: readonly string[] = fallbackRelays, fallbacks: readonly string[] = [...get(activeInboxRelays), ...get(activeOutboxRelays)],
): Promise<[Set<NDKRelay>, Set<NDKRelay>]> { ): Promise<[Set<NDKRelay>, Set<NDKRelay>]> {
const relayList = await ndk.fetchEvent( const relayList = await ndk.fetchEvent(
{ {

2
src/lib/utils/ZettelParser.ts

@ -1,7 +1,7 @@
import { ndkInstance } from "$lib/ndk"; import { ndkInstance } from "$lib/ndk";
import { signEvent, getEventHash } from "$lib/utils/nostrUtils"; import { signEvent, getEventHash } from "$lib/utils/nostrUtils";
import { getMimeTags } from "$lib/utils/mime"; import { getMimeTags } from "$lib/utils/mime";
import { standardRelays } from "$lib/consts"; import { communityRelays } from "$lib/consts";
import { nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
export interface ZettelSection { export interface ZettelSection {

82
src/lib/utils/community_checker.ts

@ -1,4 +1,4 @@
import { communityRelay } from "$lib/consts"; import { communityRelays } from "$lib/consts";
import { RELAY_CONSTANTS, SEARCH_LIMITS } from "./search_constants"; import { RELAY_CONSTANTS, SEARCH_LIMITS } from "./search_constants";
// Cache for pubkeys with kind 1 events on communityRelay // Cache for pubkeys with kind 1 events on communityRelay
@ -13,40 +13,54 @@ export async function checkCommunity(pubkey: string): Promise<boolean> {
} }
try { try {
const relayUrl = communityRelay; // Try each community relay until we find one that works
const ws = new WebSocket(relayUrl); for (const relayUrl of communityRelays) {
return await new Promise((resolve) => { try {
ws.onopen = () => { const ws = new WebSocket(relayUrl);
ws.send( const result = await new Promise<boolean>((resolve) => {
JSON.stringify([ ws.onopen = () => {
"REQ", ws.send(
RELAY_CONSTANTS.COMMUNITY_REQUEST_ID, JSON.stringify([
{ "REQ",
kinds: RELAY_CONSTANTS.COMMUNITY_REQUEST_KINDS, RELAY_CONSTANTS.COMMUNITY_REQUEST_ID,
authors: [pubkey], {
limit: SEARCH_LIMITS.COMMUNITY_CHECK, kinds: RELAY_CONSTANTS.COMMUNITY_REQUEST_KINDS,
}, authors: [pubkey],
]), limit: SEARCH_LIMITS.COMMUNITY_CHECK,
); },
}; ]),
ws.onmessage = (event) => { );
const data = JSON.parse(event.data); };
if (data[0] === "EVENT" && data[2]?.kind === 1) { ws.onmessage = (event) => {
communityCache.set(pubkey, true); const data = JSON.parse(event.data);
ws.close(); if (data[0] === "EVENT" && data[2]?.kind === 1) {
resolve(true); communityCache.set(pubkey, true);
} else if (data[0] === "EOSE") { ws.close();
communityCache.set(pubkey, false); resolve(true);
ws.close(); } else if (data[0] === "EOSE") {
resolve(false); communityCache.set(pubkey, false);
ws.close();
resolve(false);
}
};
ws.onerror = () => {
ws.close();
resolve(false);
};
});
if (result) {
return true;
} }
}; } catch {
ws.onerror = () => { // Continue to next relay if this one fails
communityCache.set(pubkey, false); continue;
ws.close(); }
resolve(false); }
};
}); // If we get here, no relay found the user
communityCache.set(pubkey, false);
return false;
} catch { } catch {
communityCache.set(pubkey, false); communityCache.set(pubkey, false);
return false; return false;

189
src/lib/utils/network_detection.ts

@ -0,0 +1,189 @@
import { deduplicateRelayUrls } from './relay_management';
/**
* Network conditions for relay selection
*/
export enum NetworkCondition {
ONLINE = 'online',
SLOW = 'slow',
OFFLINE = 'offline'
}
/**
* Network connectivity test endpoints
*/
const NETWORK_ENDPOINTS = [
'https://www.google.com/favicon.ico',
'https://httpbin.org/status/200',
'https://api.github.com/zen'
];
/**
* Detects if the network is online using more reliable endpoints
* @returns Promise that resolves to true if online, false otherwise
*/
export async function isNetworkOnline(): Promise<boolean> {
for (const endpoint of NETWORK_ENDPOINTS) {
try {
// Use a simple fetch without HEAD method to avoid CORS issues
const response = await fetch(endpoint, {
method: 'GET',
cache: 'no-cache',
signal: AbortSignal.timeout(3000),
mode: 'no-cors' // Use no-cors mode to avoid CORS issues
});
// With no-cors mode, we can't check response.ok, so we assume success if no error
return true;
} catch (error) {
console.debug(`[network_detection.ts] Failed to reach ${endpoint}:`, error);
continue;
}
}
console.debug('[network_detection.ts] All network endpoints failed');
return false;
}
/**
* Tests network speed by measuring response time
* @returns Promise that resolves to network speed in milliseconds
*/
export async function testNetworkSpeed(): Promise<number> {
const startTime = performance.now();
for (const endpoint of NETWORK_ENDPOINTS) {
try {
await fetch(endpoint, {
method: 'GET',
cache: 'no-cache',
signal: AbortSignal.timeout(5000),
mode: 'no-cors' // Use no-cors mode to avoid CORS issues
});
const endTime = performance.now();
return endTime - startTime;
} catch (error) {
console.debug(`[network_detection.ts] Speed test failed for ${endpoint}:`, error);
continue;
}
}
console.debug('[network_detection.ts] Network speed test failed for all endpoints');
return Infinity; // Very slow if it fails
}
/**
* Determines network condition based on connectivity and speed
* @returns Promise that resolves to NetworkCondition
*/
export async function detectNetworkCondition(): Promise<NetworkCondition> {
const isOnline = await isNetworkOnline();
if (!isOnline) {
console.debug('[network_detection.ts] Network condition: OFFLINE');
return NetworkCondition.OFFLINE;
}
const speed = await testNetworkSpeed();
// Consider network slow if response time > 2000ms
if (speed > 2000) {
console.debug(`[network_detection.ts] Network condition: SLOW (${speed.toFixed(0)}ms)`);
return NetworkCondition.SLOW;
}
console.debug(`[network_detection.ts] Network condition: ONLINE (${speed.toFixed(0)}ms)`);
return NetworkCondition.ONLINE;
}
/**
* Gets the appropriate relay sets based on network condition
* @param networkCondition The detected network condition
* @param discoveredLocalRelays Array of discovered local relay URLs
* @param lowbandwidthRelays Array of low bandwidth relay URLs
* @param fullRelaySet The complete relay set for normal conditions
* @returns Object with inbox and outbox relay arrays
*/
export function getRelaySetForNetworkCondition(
networkCondition: NetworkCondition,
discoveredLocalRelays: string[],
lowbandwidthRelays: string[],
fullRelaySet: { inboxRelays: string[]; outboxRelays: string[] }
): { inboxRelays: string[]; outboxRelays: string[] } {
switch (networkCondition) {
case NetworkCondition.OFFLINE:
// When offline, use local relays if available, otherwise rely on cache
// This will be improved when IndexedDB local relay is implemented
if (discoveredLocalRelays.length > 0) {
console.debug('[network_detection.ts] Using local relays (offline)');
return {
inboxRelays: discoveredLocalRelays,
outboxRelays: discoveredLocalRelays
};
} else {
console.debug('[network_detection.ts] No local relays available, will rely on cache (offline)');
return {
inboxRelays: [],
outboxRelays: []
};
}
case NetworkCondition.SLOW:
// Local relays + low bandwidth relays when slow (deduplicated)
console.debug('[network_detection.ts] Using local + low bandwidth relays (slow network)');
const slowInboxRelays = deduplicateRelayUrls([...discoveredLocalRelays, ...lowbandwidthRelays]);
const slowOutboxRelays = deduplicateRelayUrls([...discoveredLocalRelays, ...lowbandwidthRelays]);
return {
inboxRelays: slowInboxRelays,
outboxRelays: slowOutboxRelays
};
case NetworkCondition.ONLINE:
default:
// Full relay set when online
console.debug('[network_detection.ts] Using full relay set (online)');
return fullRelaySet;
}
}
/**
* Starts periodic network monitoring with reduced frequency to avoid spam
* @param onNetworkChange Callback function called when network condition changes
* @param checkInterval Interval in milliseconds between network checks (default: 60 seconds)
* @returns Function to stop the monitoring
*/
export function startNetworkMonitoring(
onNetworkChange: (condition: NetworkCondition) => void,
checkInterval: number = 60000 // Increased to 60 seconds to reduce spam
): () => void {
let lastCondition: NetworkCondition | null = null;
let intervalId: number | null = null;
const checkNetwork = async () => {
try {
const currentCondition = await detectNetworkCondition();
if (currentCondition !== lastCondition) {
console.debug(`[network_detection.ts] Network condition changed: ${lastCondition} -> ${currentCondition}`);
lastCondition = currentCondition;
onNetworkChange(currentCondition);
}
} catch (error) {
console.warn('[network_detection.ts] Network monitoring error:', error);
}
};
// Initial check
checkNetwork();
// Set up periodic monitoring
intervalId = window.setInterval(checkNetwork, checkInterval);
// Return function to stop monitoring
return () => {
if (intervalId !== null) {
clearInterval(intervalId);
intervalId = null;
}
};
}

108
src/lib/utils/nostrEventService.ts

@ -1,11 +1,13 @@
import { nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
import { getEventHash, signEvent, prefixNostrAddresses } from "./nostrUtils"; import { getEventHash, signEvent, prefixNostrAddresses } from "./nostrUtils";
import { standardRelays, fallbackRelays } from "$lib/consts"; import { communityRelays, secondaryRelays } from "$lib/consts";
import { userRelays } from "$lib/stores/relayStore";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import type { NDKEvent } from "./nostrUtils"; import type { NDKEvent } from "./nostrUtils";
import { EVENT_KINDS, TIME_CONSTANTS, TIMEOUTS } from "./search_constants"; import { EVENT_KINDS, TIME_CONSTANTS, TIMEOUTS } from "./search_constants";
import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
import { ndkInstance } from "$lib/ndk";
import { NDKRelaySet } from "@nostr-dev-kit/ndk";
export interface RootEventInfo { export interface RootEventInfo {
rootId: string; rootId: string;
@ -358,82 +360,44 @@ export async function createSignedEvent(
} }
/** /**
* Publish event to a single relay * Publishes an event to relays using the new relay management system
*/ * @param event The event to publish
async function publishToRelay( * @param relayUrls Array of relay URLs to publish to
relayUrl: string, * @returns Promise that resolves to array of successful relay URLs
signedEvent: any,
): Promise<void> {
const ws = new WebSocket(relayUrl);
return new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
ws.close();
reject(new Error("Timeout"));
}, TIMEOUTS.GENERAL);
ws.onopen = () => {
ws.send(JSON.stringify(["EVENT", signedEvent]));
};
ws.onmessage = (e) => {
const [type, id, ok, message] = JSON.parse(e.data);
if (type === "OK" && id === signedEvent.id) {
clearTimeout(timeout);
if (ok) {
ws.close();
resolve();
} else {
ws.close();
reject(new Error(message));
}
}
};
ws.onerror = () => {
clearTimeout(timeout);
ws.close();
reject(new Error("WebSocket error"));
};
});
}
/**
* Publish event to relays
*/ */
export async function publishEvent( export async function publishEvent(
signedEvent: any, event: NDKEvent,
useOtherRelays = false, relayUrls: string[],
useFallbackRelays = false, ): Promise<string[]> {
userRelayPreference = false, const successfulRelays: string[] = [];
): Promise<EventPublishResult> { const ndk = get(ndkInstance);
// Determine which relays to use
let relays = userRelayPreference ? get(userRelays) : standardRelays; if (!ndk) {
if (useOtherRelays) { throw new Error("NDK instance not available");
relays = userRelayPreference ? standardRelays : get(userRelays);
}
if (useFallbackRelays) {
relays = fallbackRelays;
} }
// Try to publish to relays // Create relay set from URLs
for (const relayUrl of relays) { const relaySet = NDKRelaySet.fromRelayUrls(relayUrls, ndk);
try {
await publishToRelay(relayUrl, signedEvent); try {
return { // Publish with timeout
success: true, await event.publish(relaySet).withTimeout(10000);
relay: relayUrl,
eventId: signedEvent.id, // For now, assume all relays were successful
}; // In a more sophisticated implementation, you'd track individual relay responses
} catch (e) { successfulRelays.push(...relayUrls);
console.error(`Failed to publish to ${relayUrl}:`, e);
} console.debug("[nostrEventService] Published event successfully:", {
eventId: event.id,
relayCount: relayUrls.length,
successfulRelays
});
} catch (error) {
console.error("[nostrEventService] Failed to publish event:", error);
throw new Error(`Failed to publish event: ${error}`);
} }
return { return successfulRelays;
success: false,
error: "Failed to publish to any relays",
};
} }
/** /**

111
src/lib/utils/nostrUtils.ts

@ -4,7 +4,8 @@ import { ndkInstance } from "$lib/ndk";
import { npubCache } from "./npubCache"; import { npubCache } from "./npubCache";
import NDK, { NDKEvent, NDKRelaySet, NDKUser } from "@nostr-dev-kit/ndk"; import NDK, { NDKEvent, NDKRelaySet, NDKUser } from "@nostr-dev-kit/ndk";
import type { NDKFilter, NDKKind } from "@nostr-dev-kit/ndk"; import type { NDKFilter, NDKKind } from "@nostr-dev-kit/ndk";
import { standardRelays, fallbackRelays, anonymousRelays } from "$lib/consts"; import { communityRelays, secondaryRelays, anonymousRelays } from "$lib/consts";
import { activeInboxRelays } from "$lib/ndk";
import { NDKRelaySet as NDKRelaySetFromNDK } from "@nostr-dev-kit/ndk"; import { NDKRelaySet as NDKRelaySetFromNDK } from "@nostr-dev-kit/ndk";
import { sha256 } from "@noble/hashes/sha256"; import { sha256 } from "@noble/hashes/sha256";
import { schnorr } from "@noble/curves/secp256k1"; import { schnorr } from "@noble/curves/secp256k1";
@ -174,9 +175,9 @@ export async function createProfileLinkWithVerification(
}; };
const allRelays = [ const allRelays = [
...standardRelays, ...communityRelays,
...userRelays, ...userRelays,
...fallbackRelays, ...secondaryRelays,
].filter((url, idx, arr) => arr.indexOf(url) === idx); ].filter((url, idx, arr) => arr.indexOf(url) === idx);
const filteredRelays = filterProblematicRelays(allRelays); const filteredRelays = filterProblematicRelays(allRelays);
@ -422,91 +423,43 @@ export async function fetchEventWithFallback(
filterOrId: string | NDKFilter<NDKKind>, filterOrId: string | NDKFilter<NDKKind>,
timeoutMs: number = 3000, timeoutMs: number = 3000,
): Promise<NDKEvent | null> { ): Promise<NDKEvent | null> {
// Get user relays if logged in // Use the active inbox relays from the relay management system
const userRelays = ndk.activeUser const inboxRelays = get(activeInboxRelays);
? Array.from(ndk.pool?.relays.values() || [])
.filter((r) => r.status === 1) // Only use connected relays // Create relay set from active inbox relays
.map((r) => r.url) const relaySet = NDKRelaySetFromNDK.fromRelayUrls(inboxRelays, ndk);
.filter((url) => !url.includes("gitcitadel.nostr1.com")) // Filter out problematic relay
: [];
// Determine which relays to use based on user authentication status
const isSignedIn = ndk.signer && ndk.activeUser;
const primaryRelays = isSignedIn ? standardRelays : anonymousRelays;
// Create three relay sets in priority order
const relaySets = [
NDKRelaySetFromNDK.fromRelayUrls(primaryRelays, ndk), // 1. Primary relays (auth or anonymous)
NDKRelaySetFromNDK.fromRelayUrls(userRelays, ndk), // 2. User relays (if logged in)
NDKRelaySetFromNDK.fromRelayUrls(fallbackRelays, ndk), // 3. fallback relays (last resort)
];
try { try {
let found: NDKEvent | null = null; if (relaySet.relays.size === 0) {
const triedRelaySets: string[] = []; console.warn("No inbox relays available for event fetch");
return null;
// Helper function to try fetching from a relay set
async function tryFetchFromRelaySet(
relaySet: NDKRelaySetFromNDK,
setName: string,
): Promise<NDKEvent | null> {
if (relaySet.relays.size === 0) return null;
triedRelaySets.push(setName);
if (
typeof filterOrId === "string" &&
new RegExp(`^[0-9a-f]{${VALIDATION.HEX_LENGTH}}$`, "i").test(filterOrId)
) {
return await ndk
.fetchEvent({ ids: [filterOrId] }, undefined, relaySet)
.withTimeout(timeoutMs);
} else {
const filter =
typeof filterOrId === "string" ? { ids: [filterOrId] } : filterOrId;
const results = await ndk
.fetchEvents(filter, undefined, relaySet)
.withTimeout(timeoutMs);
return results instanceof Set
? (Array.from(results)[0] as NDKEvent)
: null;
}
} }
// Try each relay set in order let found: NDKEvent | null = null;
for (const [index, relaySet] of relaySets.entries()) {
const setName = if (
index === 0 typeof filterOrId === "string" &&
? isSignedIn new RegExp(`^[0-9a-f]{${VALIDATION.HEX_LENGTH}}$`, "i").test(filterOrId)
? "standard relays" ) {
: "anonymous relays" found = await ndk
: index === 1 .fetchEvent({ ids: [filterOrId] }, undefined, relaySet)
? "user relays" .withTimeout(timeoutMs);
: "fallback relays"; } else {
const filter =
found = await tryFetchFromRelaySet(relaySet, setName); typeof filterOrId === "string" ? { ids: [filterOrId] } : filterOrId;
if (found) break; const results = await ndk
.fetchEvents(filter, undefined, relaySet)
.withTimeout(timeoutMs);
found = results instanceof Set
? (Array.from(results)[0] as NDKEvent)
: null;
} }
if (!found) { if (!found) {
const timeoutSeconds = timeoutMs / 1000; const timeoutSeconds = timeoutMs / 1000;
const relayUrls = relaySets const relayUrls = Array.from(relaySet.relays).map((r: any) => r.url).join(", ");
.map((set, i) => {
const setName =
i === 0
? isSignedIn
? "standard relays"
: "anonymous relays"
: i === 1
? "user relays"
: "fallback relays";
const urls = Array.from(set.relays).map((r) => r.url);
return urls.length > 0 ? `${setName} (${urls.join(", ")})` : null;
})
.filter(Boolean)
.join(", then ");
console.warn( console.warn(
`Event not found after ${timeoutSeconds}s timeout. Tried ${relayUrls}. Some relays may be offline or slow.`, `Event not found after ${timeoutSeconds}s timeout. Tried inbox relays: ${relayUrls}. Some relays may be offline or slow.`,
); );
return null; return null;
} }

4
src/lib/utils/profile_search.ts

@ -2,7 +2,7 @@ import { ndkInstance } from "$lib/ndk";
import { getUserMetadata, getNpubFromNip05 } from "$lib/utils/nostrUtils"; import { getUserMetadata, getNpubFromNip05 } from "$lib/utils/nostrUtils";
import { NDKRelaySet, NDKEvent } from "@nostr-dev-kit/ndk"; import { NDKRelaySet, NDKEvent } from "@nostr-dev-kit/ndk";
import { searchCache } from "$lib/utils/searchCache"; import { searchCache } from "$lib/utils/searchCache";
import { standardRelays, fallbackRelays } from "$lib/consts"; import { communityRelays, secondaryRelays } from "$lib/consts";
import { get } from "svelte/store"; import { get } from "svelte/store";
import type { NostrProfile, ProfileSearchResult } from "./search_types"; import type { NostrProfile, ProfileSearchResult } from "./search_types";
import { import {
@ -270,7 +270,7 @@ async function quickRelaySearch(
console.log("Normalized search term for relay search:", normalizedSearchTerm); console.log("Normalized search term for relay search:", normalizedSearchTerm);
// Use all profile relays for better coverage // Use all profile relays for better coverage
const quickRelayUrls = [...standardRelays, ...fallbackRelays]; // Use all available relays const quickRelayUrls = [...communityRelays, ...secondaryRelays]; // Use all available relays
console.log("Using all relays for search:", quickRelayUrls); console.log("Using all relays for search:", quickRelayUrls);
// Create relay sets for parallel search // Create relay sets for parallel search

8
src/lib/utils/relayDiagnostics.ts

@ -1,6 +1,7 @@
import { standardRelays, anonymousRelays, fallbackRelays } from "$lib/consts"; import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
import NDK from "@nostr-dev-kit/ndk"; import NDK from "@nostr-dev-kit/ndk";
import { TIMEOUTS } from "./search_constants"; import { TIMEOUTS } from "./search_constants";
import { get } from "svelte/store";
export interface RelayDiagnostic { export interface RelayDiagnostic {
url: string; url: string;
@ -85,9 +86,8 @@ export async function testRelay(url: string): Promise<RelayDiagnostic> {
* Tests all relays and returns diagnostic information * Tests all relays and returns diagnostic information
*/ */
export async function testAllRelays(): Promise<RelayDiagnostic[]> { export async function testAllRelays(): Promise<RelayDiagnostic[]> {
const allRelays = [ // Use the new relay management system
...new Set([...standardRelays, ...anonymousRelays, ...fallbackRelays]), const allRelays = [...get(activeInboxRelays), ...get(activeOutboxRelays)];
];
console.log("[RelayDiagnostics] Testing", allRelays.length, "relays..."); console.log("[RelayDiagnostics] Testing", allRelays.length, "relays...");

424
src/lib/utils/relay_management.ts

@ -0,0 +1,424 @@
import NDK, { NDKRelay, NDKUser } from "@nostr-dev-kit/ndk";
import { communityRelays, searchRelays, secondaryRelays, anonymousRelays, lowbandwidthRelays, localRelays } from "../consts";
import { getRelaySetForNetworkCondition, NetworkCondition } from "./network_detection";
import { networkCondition } from "../stores/networkStore";
import { get } from "svelte/store";
/**
* Normalizes a relay URL to a standard format
* @param url The relay URL to normalize
* @returns The normalized relay URL
*/
export function normalizeRelayUrl(url: string): string {
let normalized = url.toLowerCase().trim();
// Ensure protocol is present
if (!normalized.startsWith('ws://') && !normalized.startsWith('wss://')) {
normalized = 'wss://' + normalized;
}
// Remove trailing slash
normalized = normalized.replace(/\/$/, '');
return normalized;
}
/**
* Normalizes an array of relay URLs
* @param urls Array of relay URLs to normalize
* @returns Array of normalized relay URLs
*/
export function normalizeRelayUrls(urls: string[]): string[] {
return urls.map(normalizeRelayUrl);
}
/**
* Removes duplicates from an array of relay URLs
* @param urls Array of relay URLs
* @returns Array of unique relay URLs
*/
export function deduplicateRelayUrls(urls: string[]): string[] {
const normalized = normalizeRelayUrls(urls);
return [...new Set(normalized)];
}
/**
* Tests connection to a relay and returns connection status
* @param relayUrl The relay URL to test
* @param ndk The NDK instance
* @returns Promise that resolves to connection status
*/
export async function testRelayConnection(
relayUrl: string,
ndk: NDK,
): Promise<{
connected: boolean;
requiresAuth: boolean;
error?: string;
actualUrl?: string;
}> {
return new Promise((resolve) => {
// Ensure the URL is using wss:// protocol
const secureUrl = ensureSecureWebSocket(relayUrl);
// Use the existing NDK instance instead of creating a new one
const relay = new NDKRelay(secureUrl, undefined, ndk);
let authRequired = false;
let connected = false;
let error: string | undefined;
let actualUrl: string | undefined;
const timeout = setTimeout(() => {
relay.disconnect();
resolve({
connected: false,
requiresAuth: authRequired,
error: "Connection timeout",
actualUrl,
});
}, 3000); // Increased timeout to 3 seconds to give relays more time
relay.on("connect", () => {
connected = true;
actualUrl = secureUrl;
clearTimeout(timeout);
relay.disconnect();
resolve({
connected: true,
requiresAuth: authRequired,
error,
actualUrl,
});
});
relay.on("notice", (message: string) => {
if (message.includes("auth-required")) {
authRequired = true;
}
});
relay.on("disconnect", () => {
if (!connected) {
error = "Connection failed";
clearTimeout(timeout);
resolve({
connected: false,
requiresAuth: authRequired,
error,
actualUrl,
});
}
});
relay.connect();
});
}
/**
* Ensures a relay URL uses secure WebSocket protocol for remote relays
* @param url The relay URL to secure
* @returns The URL with wss:// protocol (except for localhost)
*/
function ensureSecureWebSocket(url: string): string {
// For localhost, always use ws:// (never wss://)
if (url.includes('localhost') || url.includes('127.0.0.1')) {
// Convert any wss://localhost to ws://localhost
return url.replace(/^wss:\/\//, "ws://");
}
// Replace ws:// with wss:// for remote relays
const secureUrl = url.replace(/^ws:\/\//, "wss://");
if (secureUrl !== url) {
console.warn(
`[relay_management.ts] Protocol upgrade for rem ote relay: ${url} -> ${secureUrl}`,
);
}
return secureUrl;
}
/**
* Tests connection to local relays
* @param localRelayUrls Array of local relay URLs to test
* @param ndk NDK instance
* @returns Promise that resolves to array of working local relay URLs
*/
async function testLocalRelays(localRelayUrls: string[], ndk: NDK): Promise<string[]> {
const workingRelays: string[] = [];
await Promise.all(
localRelayUrls.map(async (url) => {
try {
const result = await testRelayConnection(url, ndk);
if (result.connected) {
workingRelays.push(url);
}
} catch (error) {
// Silently ignore local relay failures
}
})
);
return workingRelays;
}
/**
* Discovers local relays by testing common localhost URLs
* @param ndk NDK instance
* @returns Promise that resolves to array of working local relay URLs
*/
export async function discoverLocalRelays(ndk: NDK): Promise<string[]> {
try {
// Convert wss:// URLs from consts to ws:// for local testing
const localRelayUrls = localRelays.map(url =>
url.replace(/^wss:\/\//, 'ws://')
);
const workingRelays = await testLocalRelays(localRelayUrls, ndk);
// If no local relays are working, return empty array
// The network detection logic will provide fallback relays
return workingRelays;
} catch (error) {
// Silently fail and return empty array
return [];
}
}
/**
* Fetches user's local relays from kind 10432 event
* @param ndk NDK instance
* @param user User to fetch local relays for
* @returns Promise that resolves to array of local relay URLs
*/
export async function getUserLocalRelays(ndk: NDK, user: NDKUser): Promise<string[]> {
try {
const localRelayEvent = await ndk.fetchEvent(
{
kinds: [10432 as any],
authors: [user.pubkey],
},
{
groupable: false,
skipVerification: false,
skipValidation: false,
}
);
if (!localRelayEvent) {
return [];
}
const localRelays: string[] = [];
localRelayEvent.tags.forEach((tag) => {
if (tag[0] === 'r' && tag[1]) {
localRelays.push(tag[1]);
}
});
return localRelays;
} catch (error) {
console.info('[relay_management.ts] Error fetching user local relays:', error);
return [];
}
}
/**
* Fetches user's blocked relays from kind 10006 event
* @param ndk NDK instance
* @param user User to fetch blocked relays for
* @returns Promise that resolves to array of blocked relay URLs
*/
export async function getUserBlockedRelays(ndk: NDK, user: NDKUser): Promise<string[]> {
try {
const blockedRelayEvent = await ndk.fetchEvent(
{
kinds: [10006],
authors: [user.pubkey],
},
{
groupable: false,
skipVerification: false,
skipValidation: false,
}
);
if (!blockedRelayEvent) {
return [];
}
const blockedRelays: string[] = [];
blockedRelayEvent.tags.forEach((tag) => {
if (tag[0] === 'r' && tag[1]) {
blockedRelays.push(tag[1]);
}
});
return blockedRelays;
} catch (error) {
console.info('[relay_management.ts] Error fetching user blocked relays:', error);
return [];
}
}
/**
* Fetches user's outbox relays from NIP-65 relay list
* @param ndk NDK instance
* @param user User to fetch outbox relays for
* @returns Promise that resolves to array of outbox relay URLs
*/
export async function getUserOutboxRelays(ndk: NDK, user: NDKUser): Promise<string[]> {
try {
const relayList = await ndk.fetchEvent(
{
kinds: [10002],
authors: [user.pubkey],
},
{
groupable: false,
skipVerification: false,
skipValidation: false,
}
);
if (!relayList) {
return [];
}
const outboxRelays: string[] = [];
relayList.tags.forEach((tag) => {
if (tag[0] === 'w' && tag[1]) {
outboxRelays.push(tag[1]);
}
});
return outboxRelays;
} catch (error) {
console.info('[relay_management.ts] Error fetching user outbox relays:', error);
return [];
}
}
/**
* Tests a set of relays in batches to avoid overwhelming them
* @param relayUrls Array of relay URLs to test
* @param ndk NDK instance
* @returns Promise that resolves to array of working relay URLs
*/
async function testRelaySet(relayUrls: string[], ndk: NDK): Promise<string[]> {
const workingRelays: string[] = [];
const maxConcurrent = 3; // Test 3 relays at a time to avoid overwhelming them
for (let i = 0; i < relayUrls.length; i += maxConcurrent) {
const batch = relayUrls.slice(i, i + maxConcurrent);
const batchPromises = batch.map(async (url) => {
try {
const result = await testRelayConnection(url, ndk);
return result.connected ? url : null;
} catch (error) {
return null;
}
});
const batchResults = await Promise.all(batchPromises);
const batchWorkingRelays = batchResults.filter((url): url is string => url !== null);
workingRelays.push(...batchWorkingRelays);
}
return workingRelays;
}
/**
* Builds a complete relay set for a user, including local, user-specific, and fallback relays
* @param ndk NDK instance
* @param user NDKUser or null for anonymous access
* @returns Promise that resolves to inbox and outbox relay arrays
*/
export async function buildCompleteRelaySet(
ndk: NDK,
user: NDKUser | null
): Promise<{ inboxRelays: string[]; outboxRelays: string[] }> {
// Discover local relays first
const discoveredLocalRelays = await discoverLocalRelays(ndk);
// Get user-specific relays if available
let userOutboxRelays: string[] = [];
let userLocalRelays: string[] = [];
let blockedRelays: string[] = [];
if (user) {
try {
userOutboxRelays = await getUserOutboxRelays(ndk, user);
} catch (error) {
// Silently ignore user relay fetch errors
}
try {
userLocalRelays = await getUserLocalRelays(ndk, user);
} catch (error) {
// Silently ignore user local relay fetch errors
}
try {
blockedRelays = await getUserBlockedRelays(ndk, user);
} catch (error) {
// Silently ignore blocked relay fetch errors
}
}
// Build initial relay sets and deduplicate
const finalInboxRelays = deduplicateRelayUrls([...discoveredLocalRelays, ...userLocalRelays]);
const finalOutboxRelays = deduplicateRelayUrls([...discoveredLocalRelays, ...userOutboxRelays]);
// Test relays and filter out non-working ones
let testedInboxRelays: string[] = [];
let testedOutboxRelays: string[] = [];
if (finalInboxRelays.length > 0) {
testedInboxRelays = await testRelaySet(finalInboxRelays, ndk);
}
if (finalOutboxRelays.length > 0) {
testedOutboxRelays = await testRelaySet(finalOutboxRelays, ndk);
}
// If no relays passed testing, use remote relays without testing
if (testedInboxRelays.length === 0 && testedOutboxRelays.length === 0) {
const remoteRelays = deduplicateRelayUrls([...secondaryRelays, ...searchRelays]);
return {
inboxRelays: remoteRelays,
outboxRelays: remoteRelays
};
}
// Use tested relays and deduplicate
const inboxRelays = testedInboxRelays.length > 0 ? deduplicateRelayUrls(testedInboxRelays) : deduplicateRelayUrls(secondaryRelays);
const outboxRelays = testedOutboxRelays.length > 0 ? deduplicateRelayUrls(testedOutboxRelays) : deduplicateRelayUrls(secondaryRelays);
// Apply network condition optimization
const currentNetworkCondition = get(networkCondition);
const networkOptimizedRelaySet = getRelaySetForNetworkCondition(
currentNetworkCondition,
discoveredLocalRelays,
lowbandwidthRelays,
{ inboxRelays, outboxRelays }
);
// Filter out blocked relays and deduplicate final sets
const finalRelaySet = {
inboxRelays: deduplicateRelayUrls(networkOptimizedRelaySet.inboxRelays.filter(r => !blockedRelays.includes(r))),
outboxRelays: deduplicateRelayUrls(networkOptimizedRelaySet.outboxRelays.filter(r => !blockedRelays.includes(r)))
};
// If no relays are working, use anonymous relays as fallback
if (finalRelaySet.inboxRelays.length === 0 && finalRelaySet.outboxRelays.length === 0) {
return {
inboxRelays: deduplicateRelayUrls(anonymousRelays),
outboxRelays: deduplicateRelayUrls(anonymousRelays)
};
}
return finalRelaySet;
}

58
src/lib/utils/subscription_search.ts

@ -3,7 +3,7 @@ import { getMatchingTags, getNpubFromNip05 } from "$lib/utils/nostrUtils";
import { nip19 } from "$lib/utils/nostrUtils"; import { nip19 } from "$lib/utils/nostrUtils";
import { NDKRelaySet, NDKEvent } from "@nostr-dev-kit/ndk"; import { NDKRelaySet, NDKEvent } from "@nostr-dev-kit/ndk";
import { searchCache } from "$lib/utils/searchCache"; import { searchCache } from "$lib/utils/searchCache";
import { communityRelay, profileRelays } from "$lib/consts"; import { communityRelays, searchRelays } from "$lib/consts";
import { get } from "svelte/store"; import { get } from "svelte/store";
import type { import type {
SearchResult, SearchResult,
@ -20,6 +20,12 @@ import {
isEmojiReaction, isEmojiReaction,
} from "./search_utils"; } from "./search_utils";
import { TIMEOUTS, SEARCH_LIMITS } from "./search_constants"; import { TIMEOUTS, SEARCH_LIMITS } from "./search_constants";
import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
// Helper function to normalize URLs for comparison
const normalizeUrl = (url: string): string => {
return url.replace(/\/$/, ''); // Remove trailing slash
};
/** /**
* Search for events by subscription type (d, t, n) * Search for events by subscription type (d, t, n)
@ -292,23 +298,40 @@ function createPrimaryRelaySet(
searchType: SearchSubscriptionType, searchType: SearchSubscriptionType,
ndk: any, ndk: any,
): NDKRelaySet { ): NDKRelaySet {
// Use the new relay management system
const searchRelays = [...get(activeInboxRelays), ...get(activeOutboxRelays)];
console.debug('subscription_search: Active relay stores:', {
inboxRelays: get(activeInboxRelays),
outboxRelays: get(activeOutboxRelays),
searchRelays
});
// Debug: Log all relays in NDK pool
const poolRelays = Array.from(ndk.pool.relays.values());
console.debug('subscription_search: NDK pool relays:', poolRelays.map((r: any) => r.url));
if (searchType === "n") { if (searchType === "n") {
// For profile searches, use profile relays first // For profile searches, use search relays first
const profileRelaySet = Array.from(ndk.pool.relays.values()).filter( const profileRelaySet = poolRelays.filter(
(relay: any) => (relay: any) =>
profileRelays.some( searchRelays.some(
(profileRelay) => (searchRelay: string) =>
relay.url === profileRelay || relay.url === profileRelay + "/", normalizeUrl(relay.url) === normalizeUrl(searchRelay),
), ),
); );
console.debug('subscription_search: Profile relay set:', profileRelaySet.map((r: any) => r.url));
return new NDKRelaySet(new Set(profileRelaySet) as any, ndk); return new NDKRelaySet(new Set(profileRelaySet) as any, ndk);
} else { } else {
// For other searches, use community relay first // For other searches, use active relays first
const communityRelaySet = Array.from(ndk.pool.relays.values()).filter( const activeRelaySet = poolRelays.filter(
(relay: any) => (relay: any) =>
relay.url === communityRelay || relay.url === communityRelay + "/", searchRelays.some(
(searchRelay: string) =>
normalizeUrl(relay.url) === normalizeUrl(searchRelay),
),
); );
return new NDKRelaySet(new Set(communityRelaySet) as any, ndk); console.debug('subscription_search: Active relay set:', activeRelaySet.map((r: any) => r.url));
return new NDKRelaySet(new Set(activeRelaySet) as any, ndk);
} }
} }
@ -511,15 +534,16 @@ async function searchOtherRelaysInBackground(
new Set( new Set(
Array.from(ndk.pool.relays.values()).filter((relay: any) => { Array.from(ndk.pool.relays.values()).filter((relay: any) => {
if (searchType === "n") { if (searchType === "n") {
// For profile searches, exclude profile relays from fallback search // For profile searches, exclude search relays from fallback search
return !profileRelays.some( return !searchRelays.some(
(profileRelay) => (searchRelay: string) =>
relay.url === profileRelay || relay.url === profileRelay + "/", normalizeUrl(relay.url) === normalizeUrl(searchRelay),
); );
} else { } else {
// For other searches, exclude community relay from fallback search // For other searches, exclude community relays from fallback search
return ( return !communityRelays.some(
relay.url !== communityRelay && relay.url !== communityRelay + "/" (communityRelay: string) =>
normalizeUrl(relay.url) === normalizeUrl(communityRelay),
); );
} }
}), }),

4
src/routes/+layout.svelte

@ -5,6 +5,7 @@
import { page } from "$app/stores"; import { page } from "$app/stores";
import { Alert } from "flowbite-svelte"; import { Alert } from "flowbite-svelte";
import { HammerSolid } from "flowbite-svelte-icons"; import { HammerSolid } from "flowbite-svelte-icons";
import { logCurrentRelayConfiguration } from "$lib/ndk";
// Get standard metadata for OpenGraph tags // Get standard metadata for OpenGraph tags
let title = "Library of Alexandria"; let title = "Library of Alexandria";
@ -18,6 +19,9 @@
onMount(() => { onMount(() => {
const rect = document.body.getBoundingClientRect(); const rect = document.body.getBoundingClientRect();
// document.body.style.height = `${rect.height}px`; // document.body.style.height = `${rect.height}px`;
// Log relay configuration when layout mounts
logCurrentRelayConfiguration();
}); });
</script> </script>

9
src/routes/+layout.ts

@ -1,5 +1,3 @@
import { feedTypeStorageKey } from "$lib/consts";
import { FeedType } from "$lib/consts";
import { getPersistedLogin, initNdk, ndkInstance } from "$lib/ndk"; import { getPersistedLogin, initNdk, ndkInstance } from "$lib/ndk";
import { import {
loginWithExtension, loginWithExtension,
@ -8,18 +6,13 @@ import {
} from "$lib/stores/userStore"; } from "$lib/stores/userStore";
import { loginMethodStorageKey } from "$lib/stores/userStore"; import { loginMethodStorageKey } from "$lib/stores/userStore";
import Pharos, { pharosInstance } from "$lib/parser"; import Pharos, { pharosInstance } from "$lib/parser";
import { feedType } from "$lib/stores";
import type { LayoutLoad } from "./$types"; import type { LayoutLoad } from "./$types";
import { get } from "svelte/store"; import { get } from "svelte/store";
export const ssr = false; export const ssr = false;
export const load: LayoutLoad = () => { export const load: LayoutLoad = () => {
const initialFeedType = // Initialize NDK with new relay management system
(localStorage.getItem(feedTypeStorageKey) as FeedType) ??
FeedType.StandardRelays;
feedType.set(initialFeedType);
const ndk = initNdk(); const ndk = initNdk();
ndkInstance.set(ndk); ndkInstance.set(ndk);

22
src/routes/+page.svelte

@ -1,14 +1,17 @@
<script lang="ts"> <script lang="ts">
import { standardRelays, fallbackRelays } from "$lib/consts";
import { Alert, Input } from "flowbite-svelte"; import { Alert, Input } from "flowbite-svelte";
import { HammerSolid } from "flowbite-svelte-icons"; import { HammerSolid } from "flowbite-svelte-icons";
import { userStore } from "$lib/stores/userStore"; import { userStore } from "$lib/stores/userStore";
import { inboxRelays, ndkSignedIn } from "$lib/ndk"; import { activeInboxRelays, ndkSignedIn } from "$lib/ndk";
import PublicationFeed from "$lib/components/publications/PublicationFeed.svelte"; import PublicationFeed from "$lib/components/publications/PublicationFeed.svelte";
let searchQuery = $state(""); let searchQuery = $state("");
let user = $state($userStore); let user = $derived($userStore);
userStore.subscribe((val) => (user = val)); let eventCount = $state({ displayed: 0, total: 0 });
function handleEventCountUpdate(counts: { displayed: number; total: number }) {
eventCount = counts;
}
</script> </script>
<Alert <Alert
@ -33,10 +36,15 @@
class="flex-grow max-w-2xl min-w-[300px] text-base" class="flex-grow max-w-2xl min-w-[300px] text-base"
/> />
</div> </div>
{#if eventCount.total > 0}
<div class="text-center text-sm text-gray-600 dark:text-gray-400">
Showing {eventCount.displayed} of {eventCount.total} events.
</div>
{/if}
<PublicationFeed <PublicationFeed
relays={standardRelays}
{fallbackRelays}
{searchQuery} {searchQuery}
userRelays={$ndkSignedIn ? $inboxRelays : []} onEventCountUpdate={handleEventCountUpdate}
/> />
</main> </main>

17
src/routes/contact/+page.svelte

@ -9,9 +9,9 @@
Input, Input,
Modal, Modal,
} from "flowbite-svelte"; } from "flowbite-svelte";
import { ndkInstance, ndkSignedIn } from "$lib/ndk"; import { ndkInstance, ndkSignedIn, activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
import { userStore } from "$lib/stores/userStore"; import { userStore } from "$lib/stores/userStore";
import { standardRelays } from "$lib/consts"; import { communityRelays } from "$lib/consts";
import type NDK from "@nostr-dev-kit/ndk"; import type NDK from "@nostr-dev-kit/ndk";
import { NDKEvent, NDKRelaySet } from "@nostr-dev-kit/ndk"; import { NDKEvent, NDKRelaySet } from "@nostr-dev-kit/ndk";
// @ts-ignore - Workaround for Svelte component import issue // @ts-ignore - Workaround for Svelte component import issue
@ -62,12 +62,13 @@
const repoAddress = const repoAddress =
"naddr1qvzqqqrhnypzplfq3m5v3u5r0q9f255fdeyz8nyac6lagssx8zy4wugxjs8ajf7pqy88wumn8ghj7mn0wvhxcmmv9uqq5stvv4uxzmnywf5kz2elajr"; "naddr1qvzqqqrhnypzplfq3m5v3u5r0q9f255fdeyz8nyac6lagssx8zy4wugxjs8ajf7pqy88wumn8ghj7mn0wvhxcmmv9uqq5stvv4uxzmnywf5kz2elajr";
// Hard-coded relays to ensure we have working relays // Use the new relay management system instead of hardcoded relays
const allRelays = [ const allRelays = [
"wss://relay.damus.io", "wss://relay.damus.io",
"wss://relay.nostr.band", "wss://relay.nostr.band",
"wss://nos.lol", "wss://nos.lol",
...standardRelays, ...$activeInboxRelays,
...$activeOutboxRelays,
]; ];
// Hard-coded repository owner pubkey and ID from the task // Hard-coded repository owner pubkey and ID from the task
@ -451,7 +452,7 @@ Also renders nostr identifiers: npubs, nprofiles, nevents, notes, and naddrs. Wi
size="xs" size="xs"
class="absolute bottom-2 right-2 z-10 opacity-60 hover:opacity-100" class="absolute bottom-2 right-2 z-10 opacity-60 hover:opacity-100"
color="light" color="light"
on:click={toggleSize} onclick={toggleSize}
> >
{isExpanded ? "⌃" : "⌄"} {isExpanded ? "⌃" : "⌄"}
</Button> </Button>
@ -459,7 +460,7 @@ Also renders nostr identifiers: npubs, nprofiles, nevents, notes, and naddrs. Wi
</div> </div>
<div class="flex justify-end space-x-4"> <div class="flex justify-end space-x-4">
<Button type="button" color="alternative" on:click={clearForm}> <Button type="button" color="alternative" onclick={clearForm}>
Clear Form Clear Form
</Button> </Button>
<Button type="submit" tabindex={0}> <Button type="submit" tabindex={0}>
@ -586,8 +587,8 @@ Also renders nostr identifiers: npubs, nprofiles, nevents, notes, and naddrs. Wi
Would you like to submit the issue? Would you like to submit the issue?
</h3> </h3>
<div class="flex justify-center gap-4"> <div class="flex justify-center gap-4">
<Button color="alternative" on:click={cancelSubmit}>Cancel</Button> <Button color="alternative" onclick={cancelSubmit}>Cancel</Button>
<Button color="primary" on:click={confirmSubmit}>Submit</Button> <Button color="primary" onclick={confirmSubmit}>Submit</Button>
</div> </div>
</div> </div>
</Modal> </Modal>

30
src/routes/events/+page.svelte

@ -13,13 +13,9 @@
import { getMatchingTags, toNpub } from "$lib/utils/nostrUtils"; import { getMatchingTags, toNpub } from "$lib/utils/nostrUtils";
import EventInput from "$lib/components/EventInput.svelte"; import EventInput from "$lib/components/EventInput.svelte";
import { userPubkey, isLoggedIn } from "$lib/stores/authStore.Svelte"; import { userPubkey, isLoggedIn } from "$lib/stores/authStore.Svelte";
import {
testAllRelays,
logRelayDiagnostics,
} from "$lib/utils/relayDiagnostics";
import CopyToClipboard from "$lib/components/util/CopyToClipboard.svelte"; import CopyToClipboard from "$lib/components/util/CopyToClipboard.svelte";
import { neventEncode, naddrEncode } from "$lib/utils"; import { neventEncode, naddrEncode } from "$lib/utils";
import { standardRelays } from "$lib/consts"; import { activeInboxRelays, activeOutboxRelays, logCurrentRelayConfiguration } from "$lib/ndk";
import { getEventType } from "$lib/utils/mime"; import { getEventType } from "$lib/utils/mime";
import ViewPublicationLink from "$lib/components/util/ViewPublicationLink.svelte"; import ViewPublicationLink from "$lib/components/util/ViewPublicationLink.svelte";
import { checkCommunity } from "$lib/utils/search_utility"; import { checkCommunity } from "$lib/utils/search_utility";
@ -246,8 +242,15 @@
return "Reference"; return "Reference";
} }
function getNeventAddress(event: NDKEvent): string { function getNeventUrl(event: NDKEvent): string {
return neventEncode(event, standardRelays); if (event.kind === 0) {
return neventEncode(event, $activeInboxRelays);
}
return neventEncode(event, $activeInboxRelays);
}
function getNaddrUrl(event: NDKEvent): string {
return naddrEncode(event, $activeInboxRelays);
} }
function isAddressableEvent(event: NDKEvent): boolean { function isAddressableEvent(event: NDKEvent): boolean {
@ -259,7 +262,7 @@
return null; return null;
} }
try { try {
return naddrEncode(event, standardRelays); return naddrEncode(event, $activeInboxRelays);
} catch { } catch {
return null; return null;
} }
@ -498,12 +501,11 @@
handleUrlChange(); handleUrlChange();
}); });
// Log relay configuration when page mounts
onMount(() => { onMount(() => {
userRelayPreference = localStorage.getItem("useUserRelays") === "true"; logCurrentRelayConfiguration();
// Run relay diagnostics to help identify connection issues
testAllRelays().then(logRelayDiagnostics).catch(console.error);
}); });
</script> </script>
<div class="w-full flex justify-center"> <div class="w-full flex justify-center">
@ -942,8 +944,8 @@
{#if event.kind !== 0} {#if event.kind !== 0}
<div class="flex flex-col gap-2 mb-4 break-all"> <div class="flex flex-col gap-2 mb-4 break-all">
<CopyToClipboard <CopyToClipboard
displayText={shortenAddress(getNeventAddress(event))} displayText={shortenAddress(getNeventUrl(event))}
copyText={getNeventAddress(event)} copyText={getNeventUrl(event)}
/> />
{#if isAddressableEvent(event)} {#if isAddressableEvent(event)}
{@const naddrAddress = getViewPublicationNaddr(event)} {@const naddrAddress = getViewPublicationNaddr(event)}

5
src/routes/publication/+page.ts

@ -2,7 +2,7 @@ import { error } from "@sveltejs/kit";
import type { Load } from "@sveltejs/kit"; import type { Load } from "@sveltejs/kit";
import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
import { getActiveRelays } from "$lib/ndk"; import { getActiveRelaySetAsNDKRelaySet } from "$lib/ndk";
import { getMatchingTags } from "$lib/utils/nostrUtils"; import { getMatchingTags } from "$lib/utils/nostrUtils";
/** /**
@ -68,10 +68,11 @@ async function fetchEventById(ndk: any, id: string): Promise<NDKEvent> {
*/ */
async function fetchEventByDTag(ndk: any, dTag: string): Promise<NDKEvent> { async function fetchEventByDTag(ndk: any, dTag: string): Promise<NDKEvent> {
try { try {
const relaySet = await getActiveRelaySetAsNDKRelaySet(ndk, true); // true for inbox relays
const event = await ndk.fetchEvent( const event = await ndk.fetchEvent(
{ "#d": [dTag] }, { "#d": [dTag] },
{ closeOnEose: false }, { closeOnEose: false },
getActiveRelays(ndk), relaySet,
); );
if (!event) { if (!event) {

Loading…
Cancel
Save