From 9a96a7aad8ce99cb355b090fd961f5b5e0c4f358 Mon Sep 17 00:00:00 2001 From: silberengel Date: Tue, 5 Aug 2025 17:46:08 +0200 Subject: [PATCH 01/16] fixed gaps in landing page feed. now always 10x column number. --- package-lock.json | 180 +++++++++--------- .../publications/PublicationFeed.svelte | 90 +++++++-- 2 files changed, 168 insertions(+), 102 deletions(-) diff --git a/package-lock.json b/package-lock.json index ed96156..003bf33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2783,19 +2783,39 @@ } }, "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "devOptional": true, + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "license": "MIT", "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": { - "node": ">= 14.16.0" + "node": ">= 8.10.0" }, "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==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" } }, "node_modules/cliui": { @@ -3711,6 +3731,16 @@ } } }, + "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, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -5953,17 +5983,27 @@ } }, "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==", - "devOptional": true, + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, "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==", + "license": "MIT", + "engines": { + "node": ">=8.6" }, "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/require-directory": { @@ -6471,6 +6511,36 @@ "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/svelte-eslint-parser": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.3.1.tgz", @@ -6671,54 +6741,6 @@ "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==", - "license": "MIT", - "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==", - "license": "ISC", - "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==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/tailwindcss/node_modules/postcss-load-config": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", @@ -6767,30 +6789,6 @@ "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==", - "license": "MIT", - "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==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -7376,13 +7374,15 @@ } }, "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, + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, "engines": { - "node": ">= 6" + "node": ">= 14.6" } }, "node_modules/yargs": { diff --git a/src/lib/components/publications/PublicationFeed.svelte b/src/lib/components/publications/PublicationFeed.svelte index 48e4eba..1e6939c 100644 --- a/src/lib/components/publications/PublicationFeed.svelte +++ b/src/lib/components/publications/PublicationFeed.svelte @@ -27,6 +27,7 @@ let loading: boolean = $state(true); let hasInitialized = $state(false); let fallbackTimeout: ReturnType | null = null; + let gridContainer: HTMLElement; // Relay management let allRelays: string[] = $state([]); @@ -35,6 +36,35 @@ // Event management let allIndexEvents: NDKEvent[] = $state([]); + // Calculate the number of columns based on window width + let columnCount = $state(1); + let publicationsToDisplay = $state(10); + + // Update column count and publications when window resizes + $effect(() => { + if (typeof window !== 'undefined') { + const width = window.innerWidth; + let newColumnCount = 1; + if (width >= 1280) newColumnCount = 4; // xl:grid-cols-4 + else if (width >= 1024) newColumnCount = 3; // lg:grid-cols-3 + else if (width >= 768) newColumnCount = 2; // md:grid-cols-2 + + if (columnCount !== newColumnCount) { + columnCount = newColumnCount; + publicationsToDisplay = newColumnCount * 10; + + // Update the view immediately when column count changes + if (allIndexEvents.length > 0) { + const source = props.searchQuery?.trim() + ? filterEventsBySearch(allIndexEvents) + : allIndexEvents; + eventsInView = source.slice(0, publicationsToDisplay); + endOfFeed = eventsInView.length >= source.length; + } + } + } + }); + // Initialize relays and fetch events async function initializeAndFetch() { if (!ndk) { @@ -121,8 +151,8 @@ `[PublicationFeed] Using cached index events (${cachedEvents.length} events)`, ); allIndexEvents = cachedEvents; - eventsInView = allIndexEvents.slice(0, 30); - endOfFeed = allIndexEvents.length <= 30; + eventsInView = allIndexEvents.slice(0, publicationsToDisplay); + endOfFeed = allIndexEvents.length <= publicationsToDisplay; loading = false; return; } @@ -210,8 +240,8 @@ allIndexEvents.sort((a, b) => b.created_at! - a.created_at!); // Update the view immediately with new events - eventsInView = allIndexEvents.slice(0, 30); - endOfFeed = allIndexEvents.length <= 30; + eventsInView = allIndexEvents.slice(0, publicationsToDisplay); + endOfFeed = allIndexEvents.length <= publicationsToDisplay; console.debug(`[PublicationFeed] Updated view with ${newEvents.length} new events from ${relay}, total: ${allIndexEvents.length}`); } @@ -236,8 +266,8 @@ indexEventCache.set(allRelays, allIndexEvents); // Final update to ensure we have the latest view - eventsInView = allIndexEvents.slice(0, 30); - endOfFeed = allIndexEvents.length <= 30; + eventsInView = allIndexEvents.slice(0, publicationsToDisplay); + endOfFeed = allIndexEvents.length <= publicationsToDisplay; loading = false; } @@ -326,11 +356,11 @@ console.debug("[PublicationFeed] Search query changed:", query); if (query && query.trim()) { const filtered = filterEventsBySearch(allIndexEvents); - eventsInView = filtered.slice(0, 30); - endOfFeed = filtered.length <= 30; + eventsInView = filtered.slice(0, publicationsToDisplay); + endOfFeed = filtered.length <= publicationsToDisplay; } else { - eventsInView = allIndexEvents.slice(0, 30); - endOfFeed = allIndexEvents.length <= 30; + eventsInView = allIndexEvents.slice(0, publicationsToDisplay); + endOfFeed = allIndexEvents.length <= publicationsToDisplay; } }, 300); @@ -354,7 +384,7 @@ let source = props.searchQuery.trim() ? filterEventsBySearch(allIndexEvents) : allIndexEvents; - eventsInView = source.slice(0, current + 30); + eventsInView = source.slice(0, current + publicationsToDisplay); endOfFeed = eventsInView.length >= source.length; loadingMore = false; } @@ -388,14 +418,50 @@ cleanup(); }); - onMount(async () => { + onMount(() => { console.debug('[PublicationFeed] onMount called'); // The effect will handle fetching when relays become available + + // Add window resize listener for responsive updates + const handleResize = () => { + if (typeof window !== 'undefined') { + const width = window.innerWidth; + let newColumnCount = 1; + if (width >= 1280) newColumnCount = 4; // xl:grid-cols-4 + else if (width >= 1024) newColumnCount = 3; // lg:grid-cols-3 + else if (width >= 768) newColumnCount = 2; // md:grid-cols-2 + + if (columnCount !== newColumnCount) { + columnCount = newColumnCount; + publicationsToDisplay = newColumnCount * 10; + + // Update the view immediately when column count changes + if (allIndexEvents.length > 0) { + const source = props.searchQuery?.trim() + ? filterEventsBySearch(allIndexEvents) + : allIndexEvents; + eventsInView = source.slice(0, publicationsToDisplay); + endOfFeed = eventsInView.length >= source.length; + } + } + } + }; + + window.addEventListener('resize', handleResize); + + // Initial calculation + handleResize(); + + // Cleanup function + return () => { + window.removeEventListener('resize', handleResize); + }; });
{#if loading && eventsInView.length === 0} From a93e528949e335d74a911f6a5e27ccdbaee12f4a Mon Sep 17 00:00:00 2001 From: silberengel Date: Tue, 5 Aug 2025 17:57:20 +0200 Subject: [PATCH 02/16] Fixed relay list and profile being empty on page refresh. --- .../publications/PublicationFeed.svelte | 11 ++++++ src/lib/components/util/Profile.svelte | 19 +++++++++- src/lib/stores/userStore.ts | 36 ++++++++++++------- src/lib/utils/nostrUtils.ts | 10 +++--- 4 files changed, 58 insertions(+), 18 deletions(-) diff --git a/src/lib/components/publications/PublicationFeed.svelte b/src/lib/components/publications/PublicationFeed.svelte index 1e6939c..7dc8b82 100644 --- a/src/lib/components/publications/PublicationFeed.svelte +++ b/src/lib/components/publications/PublicationFeed.svelte @@ -86,6 +86,17 @@ if (newRelays.length === 0) { console.debug('[PublicationFeed] No relays available, waiting...'); + // Set up a retry mechanism when relays become available + const unsubscribe = activeInboxRelays.subscribe((relays) => { + if (relays.length > 0 && !hasInitialized) { + console.debug('[PublicationFeed] Relays now available, retrying initialization'); + unsubscribe(); + setTimeout(() => { + hasInitialized = true; + initializeAndFetch(); + }, 1000); + } + }); return; } diff --git a/src/lib/components/util/Profile.svelte b/src/lib/components/util/Profile.svelte index cc5ff4a..d39c286 100644 --- a/src/lib/components/util/Profile.svelte +++ b/src/lib/components/util/Profile.svelte @@ -21,7 +21,7 @@ import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; import { onMount } from "svelte"; import { getUserMetadata } from "$lib/utils/nostrUtils"; - import { activeInboxRelays } from "$lib/ndk"; + import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; let { pubkey, isNav = false } = $props<{ pubkey?: string, isNav?: boolean }>(); @@ -187,6 +187,23 @@ try { console.log("Refreshing profile for npub:", userState.npub); + // Check if we have relays available + const inboxRelays = get(activeInboxRelays); + const outboxRelays = get(activeOutboxRelays); + + if (inboxRelays.length === 0 && outboxRelays.length === 0) { + console.log("Profile: No relays available, will retry when relays become available"); + // Set up a retry mechanism when relays become available + const unsubscribe = activeInboxRelays.subscribe((relays) => { + if (relays.length > 0 && !isRefreshingProfile) { + console.log("Profile: Relays now available, retrying profile fetch"); + unsubscribe(); + setTimeout(() => refreshProfile(), 1000); + } + }); + return; + } + // Try using NDK's built-in profile fetching first const ndk = get(ndkInstance); if (ndk && userState.ndkUser) { diff --git a/src/lib/stores/userStore.ts b/src/lib/stores/userStore.ts index df73ab7..1e58f42 100644 --- a/src/lib/stores/userStore.ts +++ b/src/lib/stores/userStore.ts @@ -288,14 +288,20 @@ export async function loginWithAmber(amberSigner: NDKSigner, user: NDKUser) { */ export async function loginWithNpub(pubkeyOrNpub: string) { const ndk = get(ndkInstance); - if (!ndk) throw new Error("NDK not initialized"); - // Only clear previous login state after successful login + if (!ndk) { + throw new Error("NDK not initialized"); + } + let hexPubkey: string; - if (pubkeyOrNpub.startsWith("npub")) { + if (pubkeyOrNpub.startsWith("npub1")) { try { - hexPubkey = nip19.decode(pubkeyOrNpub).data as string; + const decoded = nip19.decode(pubkeyOrNpub); + if (decoded.type !== "npub") { + throw new Error("Invalid npub format"); + } + hexPubkey = decoded.data; } catch (e) { - console.error("Failed to decode hex pubkey from npub:", pubkeyOrNpub, e); + console.error("Failed to decode npub:", pubkeyOrNpub, e); throw e; } } else { @@ -313,6 +319,18 @@ export async function loginWithNpub(pubkeyOrNpub: string) { const user = ndk.getUser({ npub }); let profile: NostrProfile | null = null; + + // First, update relay stores to ensure we have relays available + try { + console.debug('[userStore.ts] loginWithNpub: Updating relay stores for authenticated user'); + await updateActiveRelayStores(ndk); + } catch (error) { + console.warn('[userStore.ts] loginWithNpub: Failed to update relay stores:', error); + } + + // Wait a moment for relay stores to be properly initialized + await new Promise(resolve => setTimeout(resolve, 500)); + try { profile = await getUserMetadata(npub, true); // Force fresh fetch console.log("Login with npub - fetched profile:", profile); @@ -344,14 +362,6 @@ export async function loginWithNpub(pubkeyOrNpub: string) { userStore.set(userState); userPubkey.set(user.pubkey); - // Update relay stores with the new user's relays - try { - console.debug('[userStore.ts] loginWithNpub: Updating relay stores for authenticated user'); - await updateActiveRelayStores(ndk); - } catch (error) { - console.warn('[userStore.ts] loginWithNpub: Failed to update relay stores:', error); - } - clearLogin(); localStorage.removeItem("alexandria/logout/flag"); persistLogin(user, "npub"); diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index c36108f..d3be24d 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -5,7 +5,7 @@ import { npubCache } from "./npubCache.ts"; import NDK, { NDKEvent, NDKRelaySet, NDKUser } from "@nostr-dev-kit/ndk"; import type { NDKKind, NostrEvent } from "@nostr-dev-kit/ndk"; import type { Filter } from "./search_types.ts"; -import { communityRelays, secondaryRelays } from "../consts.ts"; +import { communityRelays, secondaryRelays, searchRelays } from "../consts.ts"; import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts"; import { NDKRelaySet as NDKRelaySetFromNDK } from "@nostr-dev-kit/ndk"; import { sha256 } from "@noble/hashes/sha2.js"; @@ -446,15 +446,17 @@ export async function fetchEventWithFallback( // Use both inbox and outbox relays for better event discovery const inboxRelays = get(activeInboxRelays); const outboxRelays = get(activeOutboxRelays); - const allRelays = [...inboxRelays, ...outboxRelays]; + let allRelays = [...inboxRelays, ...outboxRelays]; console.log("fetchEventWithFallback: Using inbox relays:", inboxRelays); console.log("fetchEventWithFallback: Using outbox relays:", outboxRelays); // Check if we have any relays available if (allRelays.length === 0) { - console.warn("fetchEventWithFallback: No relays available for event fetch"); - return null; + console.warn("fetchEventWithFallback: No relays available for event fetch, using fallback relays"); + // Use fallback relays when no relays are available + allRelays = [...secondaryRelays, ...searchRelays]; + console.log("fetchEventWithFallback: Using fallback relays:", allRelays); } // Create relay set from all available relays From 74598a1d3ce442149517bb903dbc12b4a2844463 Mon Sep 17 00:00:00 2001 From: silberengel Date: Tue, 5 Aug 2025 18:11:42 +0200 Subject: [PATCH 03/16] Added a checkbox to the landing page, so that the currently logged-in user can easily find their own publications, both as "p" tag and as "pubkey". --- .../publications/PublicationFeed.svelte | 112 +++++++++++++++--- src/routes/+page.svelte | 22 +++- 2 files changed, 115 insertions(+), 19 deletions(-) diff --git a/src/lib/components/publications/PublicationFeed.svelte b/src/lib/components/publications/PublicationFeed.svelte index 7dc8b82..5405e7e 100644 --- a/src/lib/components/publications/PublicationFeed.svelte +++ b/src/lib/components/publications/PublicationFeed.svelte @@ -13,9 +13,11 @@ import { searchCache } from "$lib/utils/searchCache"; import { indexEventCache } from "$lib/utils/indexEventCache"; import { isValidNip05Address } from "$lib/utils/search_utility"; + import { userStore } from "$lib/stores/userStore.ts"; const props = $props<{ searchQuery?: string; + showOnlyMyPublications?: boolean; onEventCountUpdate?: (counts: { displayed: number; total: number }) => void; }>(); @@ -55,9 +57,16 @@ // Update the view immediately when column count changes if (allIndexEvents.length > 0) { - const source = props.searchQuery?.trim() - ? filterEventsBySearch(allIndexEvents) - : allIndexEvents; + let source = allIndexEvents; + + // Apply user filter first + source = filterEventsByUser(source); + + // Then apply search filter if query exists + if (props.searchQuery?.trim()) { + source = filterEventsBySearch(source); + } + eventsInView = source.slice(0, publicationsToDisplay); endOfFeed = eventsInView.length >= source.length; } @@ -282,6 +291,47 @@ loading = false; } + // Function to filter events by current user's pubkey + const filterEventsByUser = (events: NDKEvent[]) => { + if (!props.showOnlyMyPublications) return events; + + const currentUser = $userStore; + if (!currentUser.signedIn || !currentUser.pubkey) { + console.debug("[PublicationFeed] User not signed in or no pubkey, showing all events"); + return events; + } + + const userPubkey = currentUser.pubkey.toLowerCase(); + console.debug("[PublicationFeed] Filtering events for user:", userPubkey); + + const filtered = events.filter((event) => { + // Check if user is the author of the event + const eventPubkey = event.pubkey.toLowerCase(); + const isAuthor = eventPubkey === userPubkey; + + // Check if user is listed in "p" tags (participants/contributors) + const pTags = getMatchingTags(event, "p"); + const isInPTags = pTags.some(tag => tag[1]?.toLowerCase() === userPubkey); + + const matches = isAuthor || isInPTags; + + if (matches) { + console.debug("[PublicationFeed] Event matches user filter:", { + id: event.id, + eventPubkey, + userPubkey, + isAuthor, + isInPTags, + pTags: pTags.map(tag => tag[1]) + }); + } + return matches; + }); + + console.debug("[PublicationFeed] Events after user filtering:", filtered.length); + return filtered; + }; + // Function to filter events based on search query const filterEventsBySearch = (events: NDKEvent[]) => { if (!props.searchQuery) return events; @@ -364,21 +414,37 @@ // Debounced search function const debouncedSearch = debounceAsync(async (query: string) => { - console.debug("[PublicationFeed] Search query changed:", query); + console.debug("[PublicationFeed] Search query or user filter changed:", query); + let filtered = allIndexEvents; + + // Apply user filter first + filtered = filterEventsByUser(filtered); + + // Then apply search filter if query exists if (query && query.trim()) { - const filtered = filterEventsBySearch(allIndexEvents); - eventsInView = filtered.slice(0, publicationsToDisplay); - endOfFeed = filtered.length <= publicationsToDisplay; - } else { - eventsInView = allIndexEvents.slice(0, publicationsToDisplay); - endOfFeed = allIndexEvents.length <= publicationsToDisplay; + filtered = filterEventsBySearch(filtered); } + + eventsInView = filtered.slice(0, publicationsToDisplay); + endOfFeed = filtered.length <= publicationsToDisplay; }, 300); + // AI-NOTE: Watch for changes in search query and user filter $effect(() => { + // Trigger search when either search query or user filter changes + // Also watch for changes in user store to update filter when user logs in/out debouncedSearch(props.searchQuery); }); + // AI-NOTE: Watch for changes in the user filter checkbox + $effect(() => { + // Trigger filtering when the user filter checkbox changes + // Access both props to ensure the effect runs when either changes + const searchQuery = props.searchQuery; + const showOnlyMyPublications = props.showOnlyMyPublications; + debouncedSearch(searchQuery); + }); + // Emit event count updates $effect(() => { if (props.onEventCountUpdate) { @@ -392,9 +458,16 @@ async function loadMorePublications() { loadingMore = true; const current = eventsInView.length; - let source = props.searchQuery.trim() - ? filterEventsBySearch(allIndexEvents) - : allIndexEvents; + let source = allIndexEvents; + + // Apply user filter first + source = filterEventsByUser(source); + + // Then apply search filter if query exists + if (props.searchQuery.trim()) { + source = filterEventsBySearch(source); + } + eventsInView = source.slice(0, current + publicationsToDisplay); endOfFeed = eventsInView.length >= source.length; loadingMore = false; @@ -448,9 +521,16 @@ // Update the view immediately when column count changes if (allIndexEvents.length > 0) { - const source = props.searchQuery?.trim() - ? filterEventsBySearch(allIndexEvents) - : allIndexEvents; + let source = allIndexEvents; + + // Apply user filter first + source = filterEventsByUser(source); + + // Then apply search filter if query exists + if (props.searchQuery?.trim()) { + source = filterEventsBySearch(source); + } + eventsInView = source.slice(0, publicationsToDisplay); endOfFeed = eventsInView.length >= source.length; } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 1439a42..038047b 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,8 +1,10 @@
From c47abe9cdc3007e240c58302adb1aefccc7acdc8 Mon Sep 17 00:00:00 2001 From: silberengel Date: Tue, 5 Aug 2025 22:41:11 +0200 Subject: [PATCH 05/16] Fixed author display on the 30040 events results --- src/lib/components/EventDetails.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index 4bd78e4..688bacd 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -405,7 +405,7 @@ Author: {@render userBadge( toNpub(event.pubkey) as string, - profile?.display_name || event.pubkey, + profile?.display_name || undefined, )} {:else} From fcdf6dfe62630ce1546920826f5bc159df540db2 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 10 Aug 2025 20:44:27 +0200 Subject: [PATCH 06/16] moved inline styles into the styles folder --- src/lib/components/Notifications.svelte | 45 ++--- src/styles/notifications.css | 214 ++++++++++++++++++++++++ 2 files changed, 237 insertions(+), 22 deletions(-) create mode 100644 src/styles/notifications.css diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index f0995c7..f1e19fa 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -1,4 +1,5 @@ {#if isOwnProfile && $userStore.signedIn} -
+
Notifications @@ -740,7 +741,7 @@

No public messages found.

{:else} -
+
{#if filteredByUser}
@@ -818,7 +819,7 @@
-
+
{isFromUser ? 'Your Message' : 'Public Message'} @@ -848,11 +849,13 @@ {/if} {#if message.content}
- {#await parseContent(message.content) then parsedContent} - {@html parsedContent} - {:catch} - {@html message.content} - {/await} +
+ {#await ((message.kind === 6 || message.kind === 16) ? parseRepostContent(message.content) : parseContent(message.content)) then parsedContent} + {@html parsedContent} + {:catch} + {@html message.content} + {/await} +
{/if} @@ -877,7 +880,7 @@

No notifications {notificationMode === "to-me" ? "received" : "sent"} found.

{:else} -
+
{#each notifications.slice(0, 100) as notification} {@const authorProfile = authorProfiles.get(notification.pubkey)}
@@ -929,11 +932,13 @@ {#if notification.content}
- {#await parseContent(notification.content) then parsedContent} - {@html parsedContent} - {:catch} - {@html truncateContent(notification.content)} - {/await} +
+ {#await ((notification.kind === 6 || notification.kind === 16) ? parseRepostContent(notification.content) : parseContent(notification.content)) then parsedContent} + {@html parsedContent} + {:catch} + {@html truncateContent(notification.content)} + {/await} +
{/if} diff --git a/src/lib/utils/notification_utils.ts b/src/lib/utils/notification_utils.ts index b0f15c9..c4e01c2 100644 --- a/src/lib/utils/notification_utils.ts +++ b/src/lib/utils/notification_utils.ts @@ -73,6 +73,48 @@ export async function parseContent(content: string): Promise { return await parseBasicmarkup(content); } +/** + * Parses repost content and renders it as an embedded event + */ +export async function parseRepostContent(content: string): Promise { + if (!content) return ""; + + try { + // Try to parse the content as JSON (repost events contain the original event as JSON) + const originalEvent = JSON.parse(content); + + // Extract the original event's content + const originalContent = originalEvent.content || ""; + const originalAuthor = originalEvent.pubkey || ""; + const originalCreatedAt = originalEvent.created_at || 0; + + // Parse the original content with basic markup + const parsedOriginalContent = await parseBasicmarkup(originalContent); + + // Create an embedded event display + const formattedDate = originalCreatedAt ? new Date(originalCreatedAt * 1000).toLocaleDateString() : "Unknown date"; + const shortAuthor = originalAuthor ? `${originalAuthor.slice(0, 8)}...${originalAuthor.slice(-4)}` : "Unknown"; + + return ` +
+
+ Reposted by: + ${shortAuthor} + + ${formattedDate} +
+
+ ${parsedOriginalContent} +
+
+ `; + } catch (error) { + // If JSON parsing fails, fall back to basic markup + console.warn("Failed to parse repost content as JSON, falling back to basic markup:", error); + return await parseBasicmarkup(content); + } +} + /** * Renders quoted content for a message */ From 3c78d6b2dcdfff535447fefa1a4c843a1a6c9f42 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 10 Aug 2025 22:42:50 +0200 Subject: [PATCH 11/16] add image expansion --- src/lib/utils/markup/basicMarkupParser.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/lib/utils/markup/basicMarkupParser.ts b/src/lib/utils/markup/basicMarkupParser.ts index 2d70c41..d4b35bd 100644 --- a/src/lib/utils/markup/basicMarkupParser.ts +++ b/src/lib/utils/markup/basicMarkupParser.ts @@ -295,10 +295,19 @@ function processBasicFormatting(content: string): string {
Image
- Embedded media - + +
`; } // Otherwise, render as a clickable link From f89bfc0b9b373628dbb25dd0fd64c42d6584afc0 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 10 Aug 2025 22:51:55 +0200 Subject: [PATCH 12/16] fixed broken publications --- src/lib/data_structures/publication_tree.ts | 140 +++++++++++++++++--- src/lib/utils/websocket_utils.ts | 10 +- 2 files changed, 128 insertions(+), 22 deletions(-) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index 6871044..c507b9f 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -2,6 +2,10 @@ import { Lazy } from "./lazy.ts"; import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type NDK from "@nostr-dev-kit/ndk"; import { fetchEventById } from "../utils/websocket_utils.ts"; +import { fetchEventWithFallback, NDKRelaySetFromNDK } from "../utils/nostrUtils.ts"; +import { get } from "svelte/store"; +import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts"; +import { searchRelays, secondaryRelays } from "../consts.ts"; enum PublicationTreeNodeType { Branch, @@ -685,24 +689,108 @@ export class PublicationTree implements AsyncIterable { if (!event) { const [kind, pubkey, dTag] = address.split(":"); - const fetchedEvent = await this.#ndk.fetchEvent({ + + // AI-NOTE: 2025-01-24 - Enhanced event fetching with comprehensive fallback + // First try to fetch using the enhanced fetchEventWithFallback function + // which includes search relay fallback logic + return fetchEventWithFallback(this.#ndk, { kinds: [parseInt(kind)], authors: [pubkey], "#d": [dTag], - }); - - // Cache the event if found - if (fetchedEvent) { - this.#eventCache.set(address, fetchedEvent); - event = fetchedEvent; - } + }, 5000) // 5 second timeout for publication events + .then(fetchedEvent => { + if (fetchedEvent) { + // Cache the event if found + this.#eventCache.set(address, fetchedEvent); + event = fetchedEvent; + } + + if (!event) { + console.warn( + `[PublicationTree] Event with address ${address} not found on primary relays, trying search relays.`, + ); + + // If still not found, try a more aggressive search using search relays + return this.#trySearchRelayFallback(address, kind, pubkey, dTag, parentNode); + } + + return this.#buildNodeFromEvent(event, address, parentNode); + }) + .catch(error => { + console.warn(`[PublicationTree] Error fetching event for address ${address}:`, error); + + // Try search relay fallback even on error + return this.#trySearchRelayFallback(address, kind, pubkey, dTag, parentNode); + }); } - if (!event) { - console.debug( - `[PublicationTree] Event with address ${address} not found.`, - ); + return Promise.resolve(this.#buildNodeFromEvent(event, address, parentNode)); + } + /** + * AI-NOTE: 2025-01-24 - Aggressive search relay fallback for publication events + * This method tries to find events on search relays when they're not found on primary relays + */ + async #trySearchRelayFallback( + address: string, + kind: string, + pubkey: string, + dTag: string, + parentNode: PublicationTreeNode + ): Promise { + try { + console.log(`[PublicationTree] Trying search relay fallback for address: ${address}`); + + // Get current relay configuration + const inboxRelays = get(activeInboxRelays); + const outboxRelays = get(activeOutboxRelays); + + // Create a comprehensive relay set including search relays + const allRelays = [...inboxRelays, ...outboxRelays, ...searchRelays, ...secondaryRelays]; + const uniqueRelays = [...new Set(allRelays)]; // Remove duplicates + + console.log(`[PublicationTree] Trying ${uniqueRelays.length} relays for fallback search:`, uniqueRelays); + + // Try each relay individually with a shorter timeout + for (const relay of uniqueRelays) { + try { + const relaySet = NDKRelaySetFromNDK.fromRelayUrls([relay], this.#ndk); + + const fetchedEvent = await this.#ndk.fetchEvent({ + kinds: [parseInt(kind)], + authors: [pubkey], + "#d": [dTag], + }, undefined, relaySet).withTimeout(3000); // 3 second timeout per relay + + if (fetchedEvent) { + console.log(`[PublicationTree] Found event ${fetchedEvent.id} on search relay: ${relay}`); + + // Cache the event + this.#eventCache.set(address, fetchedEvent); + this.#events.set(address, fetchedEvent); + + return this.#buildNodeFromEvent(fetchedEvent, address, parentNode); + } + } catch (error) { + console.debug(`[PublicationTree] Failed to fetch from relay ${relay}:`, error); + continue; // Try next relay + } + } + + // If we get here, the event was not found on any relay + console.warn(`[PublicationTree] Event with address ${address} not found on any relay after fallback search.`); + + return { + type: PublicationTreeNodeType.Leaf, + status: PublicationTreeNodeStatus.Error, + address, + parent: parentNode, + children: [], + }; + + } catch (error) { + console.error(`[PublicationTree] Error in search relay fallback for ${address}:`, error); + return { type: PublicationTreeNodeType.Leaf, status: PublicationTreeNodeStatus.Error, @@ -711,7 +799,17 @@ export class PublicationTree implements AsyncIterable { children: [], }; } + } + /** + * AI-NOTE: 2025-01-24 - Helper method to build a node from an event + * This extracts the common logic for building nodes from events + */ + #buildNodeFromEvent( + event: NDKEvent, + address: string, + parentNode: PublicationTreeNode + ): PublicationTreeNode { this.#events.set(address, event); const childAddresses = event.tags @@ -754,14 +852,11 @@ export class PublicationTree implements AsyncIterable { } }); - const resolvedAddresses = await Promise.all(eTagPromises); - const validAddresses = resolvedAddresses.filter(addr => addr !== null) as string[]; - - console.debug(`[PublicationTree] Resolved ${validAddresses.length} valid addresses from e-tags:`, validAddresses); - - if (validAddresses.length > 0) { - childAddresses.push(...validAddresses); - } + // Note: We can't await here since this is a synchronous method + // The e-tag resolution will happen when the children are processed + // For now, we'll add the e-tags as potential child addresses + const eTagAddresses = eTags.map(tag => tag[1]); + childAddresses.push(...eTagAddresses); } const node: PublicationTreeNode = { @@ -772,10 +867,13 @@ export class PublicationTree implements AsyncIterable { children: [], }; + // Add children asynchronously const childPromises = childAddresses.map(address => this.addEventByAddress(address, event) ); - await Promise.all(childPromises); + Promise.all(childPromises).catch(error => { + console.warn(`[PublicationTree] Error adding children for ${address}:`, error); + }); this.#nodeResolvedObservers.forEach((observer) => observer(address)); diff --git a/src/lib/utils/websocket_utils.ts b/src/lib/utils/websocket_utils.ts index 834bca3..aeeeb65 100644 --- a/src/lib/utils/websocket_utils.ts +++ b/src/lib/utils/websocket_utils.ts @@ -93,8 +93,16 @@ export async function fetchNostrEvent(filter: NostrFilter): Promise { + const relayPromises = uniqueRelays.map(async (relay) => { try { const ws = await WebSocketPool.instance.acquire(relay); const subId = crypto.randomUUID(); From 1283265155f3275cdb2082bef37a31a2c025b005 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 10 Aug 2025 23:18:35 +0200 Subject: [PATCH 13/16] fix the author in the publication cards --- src/lib/components/publications/PublicationHeader.svelte | 2 +- src/lib/components/util/Details.svelte | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/components/publications/PublicationHeader.svelte b/src/lib/components/publications/PublicationHeader.svelte index c1c6222..5cab792 100644 --- a/src/lib/components/publications/PublicationHeader.svelte +++ b/src/lib/components/publications/PublicationHeader.svelte @@ -35,7 +35,7 @@ let title: string = $derived(event.getMatchingTags("title")[0]?.[1]); let author: string = $derived( - event.getMatchingTags(event, "author")[0]?.[1] ?? "unknown", + event.getMatchingTags("author")[0]?.[1] ?? "unknown", ); let version: string = $derived( event.getMatchingTags("version")[0]?.[1] ?? "1", diff --git a/src/lib/components/util/Details.svelte b/src/lib/components/util/Details.svelte index 5ce8b28..ad5c423 100644 --- a/src/lib/components/util/Details.svelte +++ b/src/lib/components/util/Details.svelte @@ -62,7 +62,7 @@

{@render userBadge(event.pubkey, author)}

{@render userBadge(event.pubkey, undefined)}

From 2bc742497ebb289d6bfb211a58e75a1243e73cd0 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 10 Aug 2025 23:50:51 +0200 Subject: [PATCH 14/16] changed to Svelte 5 syntax --- src/lib/components/CommentBox.svelte | 16 ++++++++-------- src/lib/components/RelayActions.svelte | 2 +- src/lib/components/ZettelEditor.svelte | 2 +- .../components/publications/Publication.svelte | 2 +- src/routes/[...catchall]/+page.svelte | 4 ++-- src/routes/my-notes/+page.svelte | 8 ++++---- src/routes/new/compose/+page.svelte | 2 +- src/routes/new/edit/+page.svelte | 8 ++++---- 8 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/lib/components/CommentBox.svelte b/src/lib/components/CommentBox.svelte index 6952279..9d7e978 100644 --- a/src/lib/components/CommentBox.svelte +++ b/src/lib/components/CommentBox.svelte @@ -364,12 +364,12 @@
{#each markupButtons as button} - + {/each} - - +
@@ -519,12 +519,12 @@ class="mb-4" />
- Insert @@ -552,7 +552,7 @@ {error} {#if showOtherRelays} - {/if} @@ -560,7 +560,7 @@ handleSubmit(false, true)}>Try Fallback Relays {/if} @@ -604,7 +604,7 @@
{/if} diff --git a/src/lib/components/ZettelEditor.svelte b/src/lib/components/ZettelEditor.svelte index da96f74..c077398 100644 --- a/src/lib/components/ZettelEditor.svelte +++ b/src/lib/components/ZettelEditor.svelte @@ -187,7 +187,7 @@ Note content here... {:else if !isDone} - + {:else}

You've reached the end of the publication. diff --git a/src/routes/[...catchall]/+page.svelte b/src/routes/[...catchall]/+page.svelte index 0224b3d..1e3a0b1 100644 --- a/src/routes/[...catchall]/+page.svelte +++ b/src/routes/[...catchall]/+page.svelte @@ -11,13 +11,13 @@ >The page you are looking for does not exist or has been moved.

- window.history.back()}>Go Back
diff --git a/src/routes/my-notes/+page.svelte b/src/routes/my-notes/+page.svelte index 1e02ef2..4841b4c 100644 --- a/src/routes/my-notes/+page.svelte +++ b/src/routes/my-notes/+page.svelte @@ -183,7 +183,7 @@ {selectedTagTypes.has(type) ? 'border-2 border-amber-800' : 'border border-amber-200'}" - on:click={() => toggleTagType(type)} + onclick={() => toggleTagType(type)} > {#if type.length === 1} {type} @@ -200,7 +200,7 @@ {#if tagsToShow.length > 0} @@ -240,7 +240,7 @@
{getTitle(event)}
+ +
+
+ From c7e3cbf993614f2cb12ffaa2f53fcb61a57c4dd8 Mon Sep 17 00:00:00 2001 From: silberengel Date: Mon, 11 Aug 2025 07:27:05 +0200 Subject: [PATCH 16/16] switched to npub --- .../publications/PublicationFeed.svelte | 30 ++++--------------- src/lib/utils/nostrUtils.ts | 17 +++++++++-- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/lib/components/publications/PublicationFeed.svelte b/src/lib/components/publications/PublicationFeed.svelte index e95ddf5..0a2593b 100644 --- a/src/lib/components/publications/PublicationFeed.svelte +++ b/src/lib/components/publications/PublicationFeed.svelte @@ -7,6 +7,7 @@ import { onMount, onDestroy } from "svelte"; import { getMatchingTags, + toNpub, } from "$lib/utils/nostrUtils"; import { WebSocketPool } from "$lib/data_structures/websocket_pool"; import { NDKEvent } from "@nostr-dev-kit/ndk"; @@ -292,32 +293,13 @@ loading = false; } - // Function to convert various Nostr identifiers to npub + // Function to convert various Nostr identifiers to npub using the utility function const convertToNpub = (input: string): string | null => { - try { - // If it's already an npub, return it - if (input.startsWith('npub')) { - return input; - } - - // If it's a hex pubkey, convert to npub - if (input.length === 64 && /^[0-9a-fA-F]+$/.test(input)) { - return nip19.npubEncode(input); - } - - // If it's an nprofile, decode and extract npub - if (input.startsWith('nprofile')) { - const decoded = nip19.decode(input); - if (decoded.type === 'nprofile') { - return decoded.data.pubkey ? nip19.npubEncode(decoded.data.pubkey) : null; - } - } - - return null; - } catch (error) { - console.debug("[PublicationFeed] Failed to convert to npub:", input, error); - return null; + const result = toNpub(input); + if (!result) { + console.debug("[PublicationFeed] Failed to convert to npub:", input); } + return result; }; // Function to filter events by npub (author or p tags) diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index d3be24d..06ae2bf 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -519,15 +519,28 @@ export async function fetchEventWithFallback( } /** - * Converts a hex pubkey to npub, or returns npub if already encoded. + * Converts various Nostr identifiers to npub format. + * Handles hex pubkeys, npub strings, and nprofile strings. */ export function toNpub(pubkey: string | undefined): string | null { if (!pubkey) return null; try { + // If it's already an npub, return it + if (pubkey.startsWith("npub")) return pubkey; + + // If it's a hex pubkey, convert to npub if (new RegExp(`^[a-f0-9]{${VALIDATION.HEX_LENGTH}}$`, "i").test(pubkey)) { return nip19.npubEncode(pubkey); } - if (pubkey.startsWith("npub1")) return pubkey; + + // If it's an nprofile, decode and extract npub + if (pubkey.startsWith("nprofile")) { + const decoded = nip19.decode(pubkey); + if (decoded.type === 'nprofile') { + return decoded.data.pubkey ? nip19.npubEncode(decoded.data.pubkey) : null; + } + } + return null; } catch { return null;