diff --git a/src/lib/components/EventInput.svelte b/src/lib/components/EventInput.svelte index 22759a3..e8e61e5 100644 --- a/src/lib/components/EventInput.svelte +++ b/src/lib/components/EventInput.svelte @@ -18,7 +18,7 @@ removeMetadataFromContent } from "$lib/utils/asciidoc_metadata"; import { get } from "svelte/store"; - import { userPubkey } from "$lib/stores/authStore.Svelte"; + import { userStore } from "$lib/stores/userStore"; import NDK, { NDKEvent as NDKEventClass } from "@nostr-dev-kit/ndk"; import type { NDKEvent } from "$lib/utils/nostrUtils"; @@ -163,11 +163,9 @@ } function validate(): { valid: boolean; reason?: string; warning?: string } { - const currentUserPubkey = get(userPubkey as any); const userState = get(userStore); - // Try userPubkey first, then fallback to userStore - const pubkey = currentUserPubkey || userState.pubkey; + const pubkey = userState.pubkey; if (!pubkey) return { valid: false, reason: "Not logged in." }; if (!content.trim()) return { valid: false, reason: "Content required." }; @@ -221,11 +219,9 @@ createdAt = Math.floor(Date.now() / 1000); try { - const currentUserPubkey = get(userPubkey as any); const userState = get(userStore); - // Try userPubkey first, then fallback to userStore - const pubkey = currentUserPubkey || userState.pubkey; + const pubkey = userState.pubkey; if (!ndk || !pubkey) { error = "NDK or pubkey missing."; loading = false; diff --git a/src/lib/components/cards/ProfileHeader.svelte b/src/lib/components/cards/ProfileHeader.svelte index 5583cf1..7973172 100644 --- a/src/lib/components/cards/ProfileHeader.svelte +++ b/src/lib/components/cards/ProfileHeader.svelte @@ -16,6 +16,7 @@ import type { NDKEvent } from "@nostr-dev-kit/ndk"; import { goto } from "$app/navigation"; import { isPubkeyInUserLists, fetchCurrentUserLists } from "$lib/utils/user_lists"; + import { UserOutline } from "flowbite-svelte-icons"; const { event, @@ -120,9 +121,17 @@ alt="Profile avatar" class="w-16 h-16 rounded-full border flex-shrink-0" onerror={(e) => { - (e.target as HTMLImageElement).src = "/favicon.png"; + (e.target as HTMLImageElement).style.display = 'none'; + (e.target as HTMLImageElement).nextElementSibling?.classList.remove('hidden'); }} /> + + {:else} +
+ +
{/if}
diff --git a/src/lib/components/embedded_events/EmbeddedEvent.svelte b/src/lib/components/embedded_events/EmbeddedEvent.svelte index eda8a0e..7e1edee 100644 --- a/src/lib/components/embedded_events/EmbeddedEvent.svelte +++ b/src/lib/components/embedded_events/EmbeddedEvent.svelte @@ -9,6 +9,7 @@ import { getEventType } from "$lib/utils/mime"; import { nip19 } from "nostr-tools"; import { repostKinds } from "$lib/consts"; + import { UserOutline } from "flowbite-svelte-icons"; const { nostrIdentifier, @@ -317,7 +318,18 @@ src={profile.picture} alt="Profile" class="w-12 h-12 rounded-full object-cover flex-shrink-0" + onerror={(e) => { + (e.target as HTMLImageElement).style.display = 'none'; + (e.target as HTMLImageElement).nextElementSibling?.classList.remove('hidden'); + }} /> + + {:else} +
+ +
{/if} {#if profile.about}

diff --git a/src/lib/navigator/EventNetwork/NodeTooltip.svelte b/src/lib/navigator/EventNetwork/NodeTooltip.svelte index ed781c5..0200374 100644 --- a/src/lib/navigator/EventNetwork/NodeTooltip.svelte +++ b/src/lib/navigator/EventNetwork/NodeTooltip.svelte @@ -12,7 +12,7 @@ import { getDisplayNameSync, replacePubkeysWithDisplayNames, - } from "$lib/utils/profileCache"; + } from "$lib/utils/npubCache"; import {indexKind, zettelKinds, wikiKind} from "$lib/consts"; // Component props diff --git a/src/lib/navigator/EventNetwork/utils/networkBuilder.ts b/src/lib/navigator/EventNetwork/utils/networkBuilder.ts index 4e133b3..4af5497 100644 --- a/src/lib/navigator/EventNetwork/utils/networkBuilder.ts +++ b/src/lib/navigator/EventNetwork/utils/networkBuilder.ts @@ -10,7 +10,7 @@ import type { GraphData, GraphState, NetworkLink, NetworkNode } from "../types"; import { nip19 } from "nostr-tools"; import { communityRelays } from "$lib/consts"; import { getMatchingTags } from "$lib/utils/nostrUtils"; -import { getDisplayNameSync } from "$lib/utils/profileCache"; +import { getDisplayNameSync } from "$lib/utils/npubCache"; import { createDebugFunction } from "./common"; // Configuration diff --git a/src/lib/navigator/EventNetwork/utils/personNetworkBuilder.ts b/src/lib/navigator/EventNetwork/utils/personNetworkBuilder.ts index 426442e..2ac20ff 100644 --- a/src/lib/navigator/EventNetwork/utils/personNetworkBuilder.ts +++ b/src/lib/navigator/EventNetwork/utils/personNetworkBuilder.ts @@ -6,7 +6,7 @@ import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { NetworkLink, NetworkNode } from "../types"; -import { getDisplayNameSync } from "$lib/utils/profileCache"; +import { getDisplayNameSync } from "$lib/utils/npubCache"; import { createDebugFunction, SeededRandom } from "./common"; const PERSON_ANCHOR_RADIUS = 15; diff --git a/src/lib/navigator/EventNetwork/utils/tagNetworkBuilder.ts b/src/lib/navigator/EventNetwork/utils/tagNetworkBuilder.ts index e0eb13b..5d06c26 100644 --- a/src/lib/navigator/EventNetwork/utils/tagNetworkBuilder.ts +++ b/src/lib/navigator/EventNetwork/utils/tagNetworkBuilder.ts @@ -7,7 +7,7 @@ import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { GraphData, NetworkLink, NetworkNode } from "../types"; -import { getDisplayNameSync } from "$lib/utils/profileCache"; +import { getDisplayNameSync } from "$lib/utils/npubCache"; import { createDebugFunction, SeededRandom } from "./common"; // Configuration diff --git a/src/lib/ndk.ts b/src/lib/ndk.ts index 6f4d8a3..d5d0d69 100644 --- a/src/lib/ndk.ts +++ b/src/lib/ndk.ts @@ -14,7 +14,7 @@ import { testRelayConnection, } from "./utils/relay_management.ts"; import { userStore } from "./stores/userStore.ts"; -import { userPubkey } from "./stores/authStore.Svelte.ts"; + import { startNetworkStatusMonitoring, stopNetworkStatusMonitoring, @@ -837,7 +837,6 @@ export async function loginWithExtension( } activePubkey.set(signerUser.pubkey); - userPubkey.set(signerUser.pubkey); const user = ndk.getUser({ pubkey: signerUser.pubkey }); @@ -864,7 +863,6 @@ export function logout(user: NDKUser): void { clearLogin(); clearPersistedRelays(user); activePubkey.set(null); - userPubkey.set(null); ndkSignedIn.set(false); // Clear relay stores diff --git a/src/lib/stores/authStore.Svelte.ts b/src/lib/stores/authStore.Svelte.ts deleted file mode 100644 index a29917b..0000000 --- a/src/lib/stores/authStore.Svelte.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { derived, writable } from "svelte/store"; - -/** - * Stores the user's public key if logged in, or null otherwise. - */ -export const userPubkey = writable(null); - -/** - * Derived store indicating if the user is logged in. - */ -export const isLoggedIn = derived(userPubkey, ($userPubkey) => !!$userPubkey); diff --git a/src/lib/stores/userStore.ts b/src/lib/stores/userStore.ts index e69e88f..7040184 100644 --- a/src/lib/stores/userStore.ts +++ b/src/lib/stores/userStore.ts @@ -15,7 +15,7 @@ import { } from "../ndk.ts"; import { loginStorageKey } from "../consts.ts"; import { nip19 } from "nostr-tools"; -import { userPubkey } from "../stores/authStore.Svelte.ts"; + export interface UserState { pubkey: string | null; @@ -218,7 +218,6 @@ export async function loginWithExtension(ndk: NDK) { console.log("Login with extension - setting userStore with:", userState); userStore.set(userState); - userPubkey.set(user.pubkey); // Update relay stores with the new user's relays try { @@ -292,7 +291,6 @@ export async function loginWithAmber(amberSigner: NDKSigner, user: NDKUser, ndk: console.log("Login with Amber - setting userStore with:", userState); userStore.set(userState); - userPubkey.set(user.pubkey); // Update relay stores with the new user's relays try { @@ -396,7 +394,6 @@ export async function loginWithNpub(pubkeyOrNpub: string, ndk: NDK) { console.log("Login with npub - setting userStore with:", userState); userStore.set(userState); - userPubkey.set(user.pubkey); clearLogin(); // Only access localStorage on client-side @@ -470,7 +467,6 @@ export function logoutUser(ndk: NDK) { signer: null, signedIn: false, }); - userPubkey.set(null); if (ndk) { ndk.activeUser = undefined; diff --git a/src/lib/utils/cache_manager.ts b/src/lib/utils/cache_manager.ts new file mode 100644 index 0000000..813e149 --- /dev/null +++ b/src/lib/utils/cache_manager.ts @@ -0,0 +1,85 @@ +import { unifiedProfileCache } from './npubCache'; +import { searchCache } from './searchCache'; +import { indexEventCache } from './indexEventCache'; +import { clearRelaySetCache } from '../ndk'; + +/** + * Clears all application caches + * + * Clears: + * - unifiedProfileCache (profile metadata) + * - searchCache (search results) + * - indexEventCache (index events) + * - relaySetCache (relay configuration) + */ +export function clearAllCaches(): void { + console.log('[CacheManager] Clearing all application caches...'); + + // Clear in-memory caches + unifiedProfileCache.clear(); + searchCache.clear(); + indexEventCache.clear(); + clearRelaySetCache(); + + // Clear localStorage caches + clearLocalStorageCaches(); + + console.log('[CacheManager] All caches cleared successfully'); +} + +/** + * Clears profile-specific caches to force fresh profile data + * This is useful when profile pictures or metadata are stale + */ +export function clearProfileCaches(): void { + console.log('[CacheManager] Clearing profile-specific caches...'); + + // Clear unified profile cache + unifiedProfileCache.clear(); + + // Clear profile-related search results + // Note: searchCache doesn't have a way to clear specific types, so we clear all + // This is acceptable since profile searches are the most common + searchCache.clear(); + + console.log('[CacheManager] Profile caches cleared successfully'); +} + +/** + * Clears localStorage caches + */ +function clearLocalStorageCaches(): void { + if (typeof window === 'undefined') return; + + const keysToRemove: string[] = []; + + // Find all localStorage keys that start with 'alexandria' + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith('alexandria')) { + keysToRemove.push(key); + } + } + + // Remove the keys + keysToRemove.forEach(key => { + localStorage.removeItem(key); + }); + + console.log(`[CacheManager] Cleared ${keysToRemove.length} localStorage items`); +} + +/** + * Gets statistics about all caches + */ +export function getCacheStats(): { + profileCacheSize: number; + searchCacheSize: number; + indexEventCacheSize: number; +} { + return { + profileCacheSize: unifiedProfileCache.size(), + searchCacheSize: searchCache.size(), + indexEventCacheSize: indexEventCache.size(), + }; +} diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index 911ae07..f2cc770 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -1,6 +1,6 @@ import { get } from "svelte/store"; import { nip19 } from "nostr-tools"; -import { npubCache } from "./npubCache.ts"; +import { unifiedProfileCache } from "./npubCache.ts"; import NDK, { NDKEvent, NDKRelaySet, NDKUser } from "@nostr-dev-kit/ndk"; import type { NostrEvent } from "@nostr-dev-kit/ndk"; import type { Filter } from "./search_types.ts"; @@ -70,87 +70,8 @@ export async function getUserMetadata( ndk?: NDK, force = false, ): Promise { - // Remove nostr: prefix if present - const cleanId = identifier.replace(/^nostr:/, ""); - - console.log( - "getUserMetadata called with identifier:", - identifier, - "force:", - force, - ); - - if (!force && npubCache.has(cleanId)) { - const cached = npubCache.get(cleanId)!; - console.log("getUserMetadata returning cached profile:", cached); - return cached; - } - - const fallback = { name: `${cleanId.slice(0, 8)}...${cleanId.slice(-4)}` }; - - try { - if (!ndk) { - console.warn("getUserMetadata: No NDK instance available"); - npubCache.set(cleanId, fallback); - return fallback; - } - - const decoded = nip19.decode(cleanId); - if (!decoded) { - console.warn("getUserMetadata: Failed to decode identifier:", cleanId); - npubCache.set(cleanId, fallback); - return fallback; - } - - // Handle different identifier types - let pubkey: string; - if (decoded.type === "npub") { - pubkey = decoded.data; - } else if (decoded.type === "nprofile") { - pubkey = decoded.data.pubkey; - } else { - console.warn( - "getUserMetadata: Unsupported identifier type:", - decoded.type, - ); - npubCache.set(cleanId, fallback); - return fallback; - } - - console.log("getUserMetadata: Fetching profile for pubkey:", pubkey); - - const profileEvent = await fetchEventWithFallback(ndk, { - kinds: [0], - authors: [pubkey], - }); - - console.log("getUserMetadata: Profile event found:", profileEvent); - - const profile = profileEvent && profileEvent.content - ? JSON.parse(profileEvent.content) - : null; - - console.log("getUserMetadata: Parsed profile:", profile); - - const metadata: NostrProfile = { - name: profile?.name || fallback.name, - displayName: profile?.displayName || profile?.display_name, - nip05: profile?.nip05, - picture: profile?.picture || profile?.image, - about: profile?.about, - banner: profile?.banner, - website: profile?.website, - lud16: profile?.lud16, - }; - - console.log("getUserMetadata: Final metadata:", metadata); - npubCache.set(cleanId, metadata); - return metadata; - } catch (e) { - console.error("getUserMetadata: Error fetching profile:", e); - npubCache.set(cleanId, fallback); - return fallback; - } + // Use the unified profile cache which handles all relay searching and caching + return unifiedProfileCache.getProfile(identifier, ndk, force); } /** diff --git a/src/lib/utils/npubCache.ts b/src/lib/utils/npubCache.ts index 6f30764..bc50d7b 100644 --- a/src/lib/utils/npubCache.ts +++ b/src/lib/utils/npubCache.ts @@ -1,11 +1,21 @@ import type { NostrProfile } from "./search_types"; +import NDK, { NDKEvent } from "@nostr-dev-kit/ndk"; +import { fetchEventWithFallback } from "./nostrUtils"; +import { nip19 } from "nostr-tools"; export type NpubMetadata = NostrProfile; -class NpubCache { - private cache: Record = {}; - private readonly storageKey = "alexandria_npub_cache"; - private readonly maxAge = 24 * 60 * 60 * 1000; // 24 hours in milliseconds +interface CacheEntry { + profile: NpubMetadata; + timestamp: number; + pubkey: string; + relaySource?: string; +} + +class UnifiedProfileCache { + private cache: Map = new Map(); + private readonly storageKey = "alexandria_unified_profile_cache"; + private readonly maxAge = 2 * 60 * 60 * 1000; // 2 hours in milliseconds - shorter for fresher data constructor() { this.loadFromStorage(); @@ -16,86 +26,373 @@ class NpubCache { if (typeof window !== "undefined") { const stored = localStorage.getItem(this.storageKey); if (stored) { - const data = JSON.parse(stored) as Record< - string, - { profile: NpubMetadata; timestamp: number } - >; + const data = JSON.parse(stored) as Record; const now = Date.now(); // Filter out expired entries for (const [key, entry] of Object.entries(data)) { if (entry.timestamp && (now - entry.timestamp) < this.maxAge) { - this.cache[key] = entry.profile; + this.cache.set(key, entry); } } } } } catch (error) { - console.warn("Failed to load npub cache from storage:", error); + console.warn("Failed to load unified profile cache from storage:", error); } } private saveToStorage(): void { try { if (typeof window !== "undefined") { - const data: Record< - string, - { profile: NpubMetadata; timestamp: number } - > = {}; - for (const [key, profile] of Object.entries(this.cache)) { - data[key] = { profile, timestamp: Date.now() }; + const data: Record = {}; + for (const [key, entry] of this.cache.entries()) { + data[key] = entry; } localStorage.setItem(this.storageKey, JSON.stringify(data)); } } catch (error) { - console.warn("Failed to save npub cache to storage:", error); + console.warn("Failed to save unified profile cache to storage:", error); } } - get(key: string): NpubMetadata | undefined { - return this.cache[key]; + /** + * Get profile data, fetching fresh data if needed + */ + async getProfile(identifier: string, ndk?: NDK, force = false): Promise { + const cleanId = identifier.replace(/^nostr:/, ""); + + // Check cache first (unless forced) + if (!force && this.cache.has(cleanId)) { + const entry = this.cache.get(cleanId)!; + const now = Date.now(); + + // Return cached data if not expired + if ((now - entry.timestamp) < this.maxAge) { + console.log("UnifiedProfileCache: Returning cached profile:", cleanId); + return entry.profile; + } + } + + // Fetch fresh data + return this.fetchAndCacheProfile(cleanId, ndk); } - set(key: string, value: NpubMetadata): void { - this.cache[key] = value; - this.saveToStorage(); + /** + * Fetch profile from all available relays and cache it + */ + private async fetchAndCacheProfile(identifier: string, ndk?: NDK): Promise { + const fallback = { name: `${identifier.slice(0, 8)}...${identifier.slice(-4)}` }; + + try { + if (!ndk) { + console.warn("UnifiedProfileCache: No NDK instance available"); + return fallback; + } + + const decoded = nip19.decode(identifier); + if (!decoded) { + console.warn("UnifiedProfileCache: Failed to decode identifier:", identifier); + return fallback; + } + + // Handle different identifier types + let pubkey: string; + if (decoded.type === "npub") { + pubkey = decoded.data; + } else if (decoded.type === "nprofile") { + pubkey = decoded.data.pubkey; + } else { + console.warn("UnifiedProfileCache: Unsupported identifier type:", decoded.type); + return fallback; + } + + console.log("UnifiedProfileCache: Fetching fresh profile for pubkey:", pubkey); + + // Use fetchEventWithFallback to search ALL available relays + const profileEvent = await fetchEventWithFallback(ndk, { + kinds: [0], + authors: [pubkey], + }); + + if (!profileEvent || !profileEvent.content) { + console.warn("UnifiedProfileCache: No profile event found for:", pubkey); + return fallback; + } + + const profile = JSON.parse(profileEvent.content); + const metadata: NostrProfile = { + name: profile?.name || fallback.name, + displayName: profile?.displayName || profile?.display_name, + nip05: profile?.nip05, + picture: profile?.picture || profile?.image, + about: profile?.about, + banner: profile?.banner, + website: profile?.website, + lud16: profile?.lud16, + }; + + // Cache the fresh data + const entry: CacheEntry = { + profile: metadata, + timestamp: Date.now(), + pubkey: pubkey, + relaySource: profileEvent.relay?.url, + }; + + this.cache.set(identifier, entry); + this.cache.set(pubkey, entry); // Also cache by pubkey for convenience + this.saveToStorage(); + + console.log("UnifiedProfileCache: Cached fresh profile:", metadata); + return metadata; + + } catch (e) { + console.error("UnifiedProfileCache: Error fetching profile:", e); + return fallback; + } } - has(key: string): boolean { - return key in this.cache; + /** + * Get cached profile without fetching (synchronous) + */ + getCached(identifier: string): NpubMetadata | undefined { + const cleanId = identifier.replace(/^nostr:/, ""); + const entry = this.cache.get(cleanId); + + if (entry) { + const now = Date.now(); + if ((now - entry.timestamp) < this.maxAge) { + return entry.profile; + } else { + // Remove expired entry + this.cache.delete(cleanId); + } + } + + return undefined; } - delete(key: string): boolean { - if (key in this.cache) { - delete this.cache[key]; - this.saveToStorage(); - return true; + /** + * Set profile data in cache + */ + set(identifier: string, profile: NpubMetadata, pubkey?: string, relaySource?: string): void { + const cleanId = identifier.replace(/^nostr:/, ""); + const entry: CacheEntry = { + profile, + timestamp: Date.now(), + pubkey: pubkey || cleanId, + relaySource, + }; + + this.cache.set(cleanId, entry); + if (pubkey && pubkey !== cleanId) { + this.cache.set(pubkey, entry); } + this.saveToStorage(); + } + + /** + * Check if profile is cached and valid + */ + has(identifier: string): boolean { + const cleanId = identifier.replace(/^nostr:/, ""); + const entry = this.cache.get(cleanId); + + if (entry) { + const now = Date.now(); + if ((now - entry.timestamp) < this.maxAge) { + return true; + } else { + // Remove expired entry + this.cache.delete(cleanId); + } + } + return false; } - deleteMany(keys: string[]): number { - let deleted = 0; - for (const key of keys) { - if (this.delete(key)) { - deleted++; + /** + * Remove profile from cache + */ + delete(identifier: string): boolean { + const cleanId = identifier.replace(/^nostr:/, ""); + const entry = this.cache.get(cleanId); + + if (entry) { + this.cache.delete(cleanId); + if (entry.pubkey && entry.pubkey !== cleanId) { + this.cache.delete(entry.pubkey); } + this.saveToStorage(); + return true; } - return deleted; + + return false; } + /** + * Clear all cached profiles + */ clear(): void { - this.cache = {}; + this.cache.clear(); this.saveToStorage(); } + /** + * Get cache size + */ size(): number { - return Object.keys(this.cache).length; + return this.cache.size; } + /** + * Get all cached profiles + */ getAll(): Record { - return { ...this.cache }; + const result: Record = {}; + for (const [key, entry] of this.cache.entries()) { + result[key] = entry.profile; + } + return result; } + + /** + * Clean up expired entries + */ + cleanup(): void { + const now = Date.now(); + const expiredKeys: string[] = []; + + for (const [key, entry] of this.cache.entries()) { + if ((now - entry.timestamp) >= this.maxAge) { + expiredKeys.push(key); + } + } + + expiredKeys.forEach(key => this.cache.delete(key)); + + if (expiredKeys.length > 0) { + this.saveToStorage(); + console.log(`UnifiedProfileCache: Cleaned up ${expiredKeys.length} expired entries`); + } + } +} + +// Export the unified cache instance +export const unifiedProfileCache = new UnifiedProfileCache(); + +// Clean up expired entries every 30 minutes +if (typeof window !== "undefined") { + setInterval(() => { + unifiedProfileCache.cleanup(); + }, 30 * 60 * 1000); +} + +// Legacy compatibility - keep the old npubCache for backward compatibility +// but make it use the unified cache internally +export const npubCache = { + get: (key: string) => unifiedProfileCache.getCached(key), + set: (key: string, value: NpubMetadata) => unifiedProfileCache.set(key, value), + has: (key: string) => unifiedProfileCache.has(key), + delete: (key: string) => unifiedProfileCache.delete(key), + clear: () => unifiedProfileCache.clear(), + size: () => unifiedProfileCache.size(), + getAll: () => unifiedProfileCache.getAll(), +}; + +// Legacy compatibility for old profileCache functions +export async function getDisplayName(pubkey: string, ndk: NDK): Promise { + const profile = await unifiedProfileCache.getProfile(pubkey, ndk); + return profile.displayName || profile.name || `${pubkey.slice(0, 8)}...${pubkey.slice(-4)}`; } -export const npubCache = new NpubCache(); +export function getDisplayNameSync(pubkey: string): string { + const profile = unifiedProfileCache.getCached(pubkey); + return profile?.displayName || profile?.name || `${pubkey.slice(0, 8)}...${pubkey.slice(-4)}`; +} + +export async function batchFetchProfiles( + pubkeys: string[], + ndk: NDK, + onProgress?: (fetched: number, total: number) => void, +): Promise { + const allProfileEvents: NDKEvent[] = []; + + if (onProgress) onProgress(0, pubkeys.length); + + // Fetch profiles in parallel using the unified cache + const fetchPromises = pubkeys.map(async (pubkey, index) => { + try { + const profile = await unifiedProfileCache.getProfile(pubkey, ndk); + if (onProgress) onProgress(index + 1, pubkeys.length); + + // Create a mock NDKEvent for compatibility + const event = new NDKEvent(ndk); + event.content = JSON.stringify(profile); + event.pubkey = pubkey; + return event; + } catch (e) { + console.error(`Failed to fetch profile for ${pubkey}:`, e); + return null; + } + }); + + const results = await Promise.allSettled(fetchPromises); + results.forEach(result => { + if (result.status === 'fulfilled' && result.value) { + allProfileEvents.push(result.value); + } + }); + + return allProfileEvents; +} + +export function extractPubkeysFromEvents(events: NDKEvent[]): Set { + const pubkeys = new Set(); + + events.forEach((event) => { + // Add author pubkey + if (event.pubkey) { + pubkeys.add(event.pubkey); + } + + // Add pubkeys from p tags + const pTags = event.getMatchingTags("p"); + pTags.forEach((tag) => { + if (tag[1]) { + pubkeys.add(tag[1]); + } + }); + + // Extract pubkeys from content (nostr:npub1... format) + const npubPattern = /nostr:npub1[a-z0-9]{58}/g; + const matches = event.content?.match(npubPattern) || []; + matches.forEach((match) => { + try { + const npub = match.replace("nostr:", ""); + const decoded = nip19.decode(npub); + if (decoded.type === "npub") { + pubkeys.add(decoded.data as string); + } + } catch (e) { + // Invalid npub, ignore + } + }); + }); + + return pubkeys; +} + +export function clearProfileCache(): void { + unifiedProfileCache.clear(); +} + +export function replacePubkeysWithDisplayNames(text: string): string { + // Match hex pubkeys (64 characters) + const pubkeyRegex = /\b[0-9a-fA-F]{64}\b/g; + + return text.replace(pubkeyRegex, (match) => { + return getDisplayNameSync(match); + }); +} diff --git a/src/lib/utils/profileCache.ts b/src/lib/utils/profileCache.ts deleted file mode 100644 index 08fea96..0000000 --- a/src/lib/utils/profileCache.ts +++ /dev/null @@ -1,257 +0,0 @@ -import NDK, { type NDKEvent } from "@nostr-dev-kit/ndk"; -import { nip19 } from "nostr-tools"; -import { toNpub } from "./nostrUtils"; - -interface ProfileData { - display_name?: string; - name?: string; - picture?: string; - about?: string; -} - -// Cache for user profiles -const profileCache = new Map(); - -/** - * Fetches profile data for a pubkey - * @param pubkey - The public key to fetch profile for - * @returns Profile data or null if not found - */ -async function fetchProfile(pubkey: string, ndk: NDK): Promise { - try { - const profileEvents = await ndk.fetchEvents({ - kinds: [0], - authors: [pubkey], - limit: 1, - }); - - if (profileEvents.size === 0) { - return null; - } - - // Get the most recent profile event - const profileEvent = Array.from(profileEvents)[0]; - - try { - const content = JSON.parse(profileEvent.content); - return content as ProfileData; - } catch (e) { - console.error("Failed to parse profile content:", e); - return null; - } - } catch (e) { - console.error("Failed to fetch profile:", e); - return null; - } -} - -/** - * Gets the display name for a pubkey, using cache - * @param pubkey - The public key to get display name for - * @returns Display name, name, or shortened npub (never hex ID) - */ -export async function getDisplayName(pubkey: string, ndk: NDK): Promise { - // Check cache first - if (profileCache.has(pubkey)) { - const profile = profileCache.get(pubkey)!; - const npub = toNpub(pubkey); - return profile.display_name || profile.name || (npub ? shortenNpub(npub) : pubkey); - } - - // Fetch profile - const profile = await fetchProfile(pubkey, ndk); - if (profile) { - profileCache.set(pubkey, profile); - const npub = toNpub(pubkey); - return profile.display_name || profile.name || (npub ? shortenNpub(npub) : pubkey); - } - - // Fallback to shortened npub or pubkey - const npub = toNpub(pubkey); - return npub ? shortenNpub(npub) : pubkey; -} - -/** - * Batch fetches profiles for multiple pubkeys - * @param pubkeys - Array of public keys to fetch profiles for - * @param onProgress - Optional callback for progress updates - * @returns Array of profile events - */ -export async function batchFetchProfiles( - pubkeys: string[], - ndk: NDK, - onProgress?: (fetched: number, total: number) => void, -): Promise { - const allProfileEvents: NDKEvent[] = []; - - // Filter out already cached pubkeys - const uncachedPubkeys = pubkeys.filter((pk) => !profileCache.has(pk)); - - if (uncachedPubkeys.length === 0) { - if (onProgress) onProgress(pubkeys.length, pubkeys.length); - return allProfileEvents; - } - - try { - // Report initial progress - const cachedCount = pubkeys.length - uncachedPubkeys.length; - if (onProgress) onProgress(cachedCount, pubkeys.length); - - // Batch fetch in chunks to avoid overwhelming relays - const CHUNK_SIZE = 50; - let fetchedCount = cachedCount; - - for (let i = 0; i < uncachedPubkeys.length; i += CHUNK_SIZE) { - const chunk = uncachedPubkeys.slice( - i, - Math.min(i + CHUNK_SIZE, uncachedPubkeys.length), - ); - - const profileEvents = await ndk.fetchEvents({ - kinds: [0], - authors: chunk, - }); - - // Process each profile event - profileEvents.forEach((event: NDKEvent) => { - try { - const content = JSON.parse(event.content); - profileCache.set(event.pubkey, content as ProfileData); - allProfileEvents.push(event); - fetchedCount++; - } catch (e) { - console.error("Failed to parse profile content:", e); - } - }); - - // Update progress - if (onProgress) { - onProgress(fetchedCount, pubkeys.length); - } - } - - // Final progress update - if (onProgress) onProgress(pubkeys.length, pubkeys.length); - } catch (e) { - console.error("Failed to batch fetch profiles:", e); - } - - return allProfileEvents; -} - -/** - * Gets display name synchronously from cache - * @param pubkey - The public key to get display name for - * @returns Display name, name, or shortened npub (never hex ID) - */ -export function getDisplayNameSync(pubkey: string): string { - if (profileCache.has(pubkey)) { - const profile = profileCache.get(pubkey)!; - const npub = toNpub(pubkey); - return profile.display_name || profile.name || (npub ? shortenNpub(npub) : pubkey); - } - const npub = toNpub(pubkey); - return npub ? shortenNpub(npub) : pubkey; -} - -/** - * Shortens an npub for display - * @param npub - The npub to shorten - * @returns Shortened npub (first 8 chars...last 4 chars) - */ -function shortenNpub(npub: string): string { - if (npub.length <= 12) return npub; - return `${npub.slice(0, 8)}...${npub.slice(-4)}`; -} - -/** - * Clears the profile cache - */ -export function clearProfileCache(): void { - profileCache.clear(); -} - -/** - * Extracts all pubkeys from events (authors and p tags) - * @param events - Array of events to extract pubkeys from - * @returns Set of unique pubkeys - */ -export function extractPubkeysFromEvents(events: NDKEvent[]): Set { - const pubkeys = new Set(); - - events.forEach((event) => { - // Add author pubkey - if (event.pubkey) { - pubkeys.add(event.pubkey); - } - - // Add pubkeys from p tags - const pTags = event.getMatchingTags("p"); - pTags.forEach((tag) => { - if (tag[1]) { - pubkeys.add(tag[1]); - } - }); - - // Extract pubkeys from content (nostr:npub1... format) - const npubPattern = /nostr:npub1[a-z0-9]{58}/g; - const matches = event.content?.match(npubPattern) || []; - matches.forEach((match) => { - try { - const npub = match.replace("nostr:", ""); - const decoded = nip19.decode(npub); - if (decoded.type === "npub") { - pubkeys.add(decoded.data as string); - } - } catch (e) { - // Invalid npub, ignore - } - }); - }); - - return pubkeys; -} - -/** - * Replaces pubkeys in content with display names - * @param content - The content to process - * @returns Content with pubkeys replaced by display names - */ -export function replaceContentPubkeys(content: string): string { - if (!content) return content; - - // Replace nostr:npub1... references - const npubPattern = /nostr:npub[a-z0-9]{58}/g; - let result = content; - - const matches = content.match(npubPattern) || []; - matches.forEach((match) => { - try { - const npub = match.replace("nostr:", ""); - const decoded = nip19.decode(npub); - if (decoded.type === "npub") { - const pubkey = decoded.data as string; - const displayName = getDisplayNameSync(pubkey); - result = result.replace(match, `@${displayName}`); - } - } catch (e) { - // Invalid npub, leave as is - } - }); - - return result; -} - -/** - * Replaces pubkey references in text with display names - * @param text - Text that may contain pubkey references - * @returns Text with pubkeys replaced by display names - */ -export function replacePubkeysWithDisplayNames(text: string): string { - // Match hex pubkeys (64 characters) - const pubkeyRegex = /\b[0-9a-fA-F]{64}\b/g; - - return text.replace(pubkeyRegex, (match) => { - return getDisplayNameSync(match); - }); -} diff --git a/src/lib/utils/profile_search.ts b/src/lib/utils/profile_search.ts index 433fdb8..550f87d 100644 --- a/src/lib/utils/profile_search.ts +++ b/src/lib/utils/profile_search.ts @@ -1,8 +1,8 @@ -import { activeInboxRelays } from "../ndk.ts"; -import { getNpubFromNip05, getUserMetadata } from "./nostrUtils.ts"; +import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts"; +import { getNpubFromNip05, getUserMetadata, fetchEventWithFallback } from "./nostrUtils.ts"; import NDK, { NDKEvent, NDKRelaySet } from "@nostr-dev-kit/ndk"; import { searchCache } from "./searchCache.ts"; -import { communityRelays, searchRelays, secondaryRelays } from "../consts.ts"; +import { communityRelays, searchRelays, secondaryRelays, anonymousRelays } from "../consts.ts"; import { get } from "svelte/store"; import type { NostrProfile, ProfileSearchResult } from "./search_types.ts"; import { @@ -11,6 +11,7 @@ import { nip05Matches, normalizeSearchTerm, } from "./search_utils.ts"; +import { nip19 } from "nostr-tools"; /** * Search for profiles by various criteria (display name, name, NIP-05, npub) @@ -77,9 +78,29 @@ export async function searchProfiles( const npub = await getNpubFromNip05(normalizedNip05); if (npub) { const metadata = await getUserMetadata(npub, ndk); - const profile: NostrProfile = { + + // AI-NOTE: 2025-01-24 - Fetch the original event timestamp to preserve created_at + let created_at: number | undefined = undefined; + try { + const decoded = nip19.decode(npub); + if (decoded.type === "npub") { + const pubkey = decoded.data as string; + const originalEvent = await fetchEventWithFallback(ndk, { + kinds: [0], + authors: [pubkey], + }); + if (originalEvent && originalEvent.created_at) { + created_at = originalEvent.created_at; + } + } + } catch (e) { + console.warn("profile_search: Failed to fetch original event timestamp:", e); + } + + const profile: NostrProfile & { created_at?: number } = { ...metadata, pubkey: npub, + created_at: created_at, }; foundProfiles = [profile]; } @@ -186,9 +207,29 @@ async function searchNip05Domains( npub, ); const metadata = await getUserMetadata(npub, ndk); - const profile: NostrProfile = { + + // AI-NOTE: 2025-01-24 - Fetch the original event timestamp to preserve created_at + let created_at: number | undefined = undefined; + try { + const decoded = nip19.decode(npub); + if (decoded.type === "npub") { + const pubkey = decoded.data as string; + const originalEvent = await fetchEventWithFallback(ndk, { + kinds: [0], + authors: [pubkey], + }); + if (originalEvent && originalEvent.created_at) { + created_at = originalEvent.created_at; + } + } + } catch (e) { + console.warn("profile_search: Failed to fetch original event timestamp:", e); + } + + const profile: NostrProfile & { created_at?: number } = { ...metadata, pubkey: npub, + created_at: created_at, }; console.log( "NIP-05 search: created profile for gitcitadel.com:", @@ -218,9 +259,29 @@ async function searchNip05Domains( if (npub) { console.log("NIP-05 search: found npub for", nip05Address, ":", npub); const metadata = await getUserMetadata(npub, ndk); - const profile: NostrProfile = { + + // AI-NOTE: 2025-01-24 - Fetch the original event timestamp to preserve created_at + let created_at: number | undefined = undefined; + try { + const decoded = nip19.decode(npub); + if (decoded.type === "npub") { + const pubkey = decoded.data as string; + const originalEvent = await fetchEventWithFallback(ndk, { + kinds: [0], + authors: [pubkey], + }); + if (originalEvent && originalEvent.created_at) { + created_at = originalEvent.created_at; + } + } + } catch (e) { + console.warn("profile_search: Failed to fetch original event timestamp:", e); + } + + const profile: NostrProfile & { created_at?: number } = { ...metadata, pubkey: npub, + created_at: created_at, }; console.log( "NIP-05 search: created profile for", @@ -253,7 +314,7 @@ async function searchNip05Domains( } /** - * Quick relay search with short timeout + * Search for profiles across all available relays */ async function quickRelaySearch( searchTerm: string, @@ -265,18 +326,29 @@ async function quickRelaySearch( const normalizedSearchTerm = normalizeSearchTerm(searchTerm); console.log("Normalized search term for relay search:", normalizedSearchTerm); - // Use search relays (optimized for profiles) + user's inbox relays + community relays + // AI-NOTE: 2025-01-24 - Use ALL available relays for comprehensive profile discovery + // This ensures we don't miss profiles due to stale cache or limited relay coverage + + // Get all available relays from NDK pool (most comprehensive) + const poolRelays = Array.from(ndk.pool.relays.values()).map((r: any) => r.url) as string[]; const userInboxRelays = get(activeInboxRelays); - const quickRelayUrls = [ - ...searchRelays, // Dedicated profile search relays + const userOutboxRelays = get(activeOutboxRelays); + + // Combine ALL available relays for maximum coverage + const allRelayUrls = [ + ...poolRelays, // All NDK pool relays ...userInboxRelays, // User's personal inbox relays + ...userOutboxRelays, // User's personal outbox relays + ...searchRelays, // Dedicated profile search relays ...communityRelays, // Community relays ...secondaryRelays, // Secondary relays as fallback + ...anonymousRelays, // Anonymous relays as additional fallback ]; // Deduplicate relay URLs - const uniqueRelayUrls = [...new Set(quickRelayUrls)]; - console.log("Using relays for profile search:", uniqueRelayUrls); + const uniqueRelayUrls = [...new Set(allRelayUrls)]; + console.log("Using ALL available relays for profile search:", uniqueRelayUrls); + console.log("Total relays for profile search:", uniqueRelayUrls.length); // Create relay sets for parallel search const relaySets = uniqueRelayUrls @@ -290,6 +362,8 @@ async function quickRelaySearch( }) .filter(Boolean); + console.log("Created relay sets for profile search:", relaySets.length); + // Search all relays in parallel with short timeout const searchPromises = relaySets.map((relaySet, index) => { if (!relaySet) return []; @@ -346,6 +420,7 @@ async function quickRelaySearch( nip05: profileData.nip05, pubkey: event.pubkey, searchTerm: normalizedSearchTerm, + relay: uniqueRelayUrls[index], }); const profile = createProfileFromEvent(event, profileData); diff --git a/src/lib/utils/search_types.ts b/src/lib/utils/search_types.ts index 8dd688a..46da61e 100644 --- a/src/lib/utils/search_types.ts +++ b/src/lib/utils/search_types.ts @@ -64,6 +64,8 @@ export type SearchSubscriptionType = "d" | "t" | "n"; export interface SearchFilter { filter: Filter; subscriptionType: string; + searchTerm?: string; // AI-NOTE: 2025-01-24 - Optional search term for client-side filtering + preloadedEvents?: NDKEvent[]; // AI-NOTE: 2025-01-24 - Preloaded events for profile searches } /** diff --git a/src/lib/utils/subscription_search.ts b/src/lib/utils/subscription_search.ts index 699eae9..bb6d692 100644 --- a/src/lib/utils/subscription_search.ts +++ b/src/lib/utils/subscription_search.ts @@ -59,15 +59,49 @@ export async function searchBySubscription( const cachedResult = searchCache.get(searchType, normalizedSearchTerm); if (cachedResult) { console.log("subscription_search: Found cached result:", cachedResult); + + // AI-NOTE: 2025-01-24 - Ensure cached events have created_at property preserved + // This fixes the "Unknown date" issue when events are retrieved from cache + const eventsWithCreatedAt = cachedResult.events.map(event => { + if (event && typeof event === 'object' && !event.created_at) { + console.warn("subscription_search: Event missing created_at, setting to 0:", event.id); + (event as any).created_at = 0; + } + return event; + }); + + const secondOrderWithCreatedAt = cachedResult.secondOrder.map(event => { + if (event && typeof event === 'object' && !event.created_at) { + console.warn("subscription_search: Second order event missing created_at, setting to 0:", event.id); + (event as any).created_at = 0; + } + return event; + }); + + const tTagEventsWithCreatedAt = cachedResult.tTagEvents.map(event => { + if (event && typeof event === 'object' && !event.created_at) { + console.warn("subscription_search: T-tag event missing created_at, setting to 0:", event.id); + (event as any).created_at = 0; + } + return event; + }); + + const resultWithCreatedAt = { + ...cachedResult, + events: eventsWithCreatedAt, + secondOrder: secondOrderWithCreatedAt, + tTagEvents: tTagEventsWithCreatedAt + }; + // AI-NOTE: 2025-01-24 - For profile searches, return cached results immediately // The EventSearch component now handles cache checking before calling this function if (searchType === "n") { console.log( "subscription_search: Returning cached profile result immediately", ); - return cachedResult; + return resultWithCreatedAt; } else { - return cachedResult; + return resultWithCreatedAt; } } @@ -96,6 +130,7 @@ export async function searchBySubscription( const searchFilter = await createSearchFilter( searchType, normalizedSearchTerm, + ndk, ); console.log("subscription_search: Created search filter:", searchFilter); const primaryRelaySet = createPrimaryRelaySet(searchType, ndk); @@ -105,6 +140,31 @@ export async function searchBySubscription( "relays", ); + // AI-NOTE: 2025-01-24 - Check for preloaded events first (for profile searches) + if (searchFilter.preloadedEvents && searchFilter.preloadedEvents.length > 0) { + console.log("subscription_search: Using preloaded events:", searchFilter.preloadedEvents.length); + processPrimaryRelayResults( + new Set(searchFilter.preloadedEvents), + searchType, + searchFilter.subscriptionType, + normalizedSearchTerm, + searchState, + abortSignal, + cleanup, + ); + + if (hasResults(searchState, searchType)) { + console.log("subscription_search: Found results from preloaded events, returning immediately"); + const immediateResult = createSearchResult( + searchState, + searchType, + normalizedSearchTerm, + ); + searchCache.set(searchType, normalizedSearchTerm, immediateResult); + return immediateResult; + } + } + // Phase 1: Search primary relay if (primaryRelaySet.relays.size > 0) { try { @@ -338,6 +398,7 @@ function createCleanupFunction(searchState: any) { async function createSearchFilter( searchType: SearchSubscriptionType, normalizedSearchTerm: string, + ndk: NDK, ): Promise { console.log("subscription_search: Creating search filter for:", { searchType, @@ -368,8 +429,54 @@ async function createSearchFilter( return tFilter; } case "n": { - const nFilter = await createProfileSearchFilter(normalizedSearchTerm); - console.log("subscription_search: Created profile filter:", nFilter); + // AI-NOTE: 2025-01-24 - Use the existing profile search functionality + // This properly handles NIP-05 lookups and name searches + const { searchProfiles } = await import("./profile_search.ts"); + const profileResult = await searchProfiles(normalizedSearchTerm, ndk); + + // Convert profile results to events for compatibility + const events = profileResult.profiles.map((profile) => { + const event = new NDKEvent(ndk); + event.content = JSON.stringify(profile); + + // AI-NOTE: 2025-01-24 - Convert npub to hex public key for compatibility with nprofileEncode + // The profile.pubkey is an npub (bech32-encoded), but nprofileEncode expects hex-encoded public key + let hexPubkey = profile.pubkey || ""; + if (profile.pubkey && profile.pubkey.startsWith("npub")) { + try { + const decoded = nip19.decode(profile.pubkey); + if (decoded.type === "npub") { + hexPubkey = decoded.data as string; + } + } catch (e) { + console.warn("subscription_search: Failed to decode npub:", profile.pubkey, e); + } + } + event.pubkey = hexPubkey; + event.kind = 0; + + // AI-NOTE: 2025-01-24 - Use the preserved created_at timestamp from the profile + // This ensures the profile cards show the actual creation date instead of "Unknown date" + if ((profile as any).created_at) { + event.created_at = (profile as any).created_at; + console.log("subscription_search: Using preserved timestamp:", event.created_at); + } else { + // Fallback to current timestamp if no preserved timestamp + event.created_at = Math.floor(Date.now() / 1000); + console.log("subscription_search: Using fallback timestamp:", event.created_at); + } + + return event; + }); + + // Return a mock filter since we're using the profile search directly + const nFilter = { + filter: { kinds: [0], limit: 1 }, // Dummy filter + subscriptionType: "profile-search", + searchTerm: normalizedSearchTerm, + preloadedEvents: events, // AI-NOTE: 2025-01-24 - Pass preloaded events + }; + console.log("subscription_search: Created profile filter with preloaded events:", nFilter); return nFilter; } default: { @@ -378,58 +485,7 @@ async function createSearchFilter( } } -/** - * Create profile search filter - */ -async function createProfileSearchFilter( - normalizedSearchTerm: string, -): Promise { - // For npub searches, try to decode the search term first - try { - const decoded = nip19.decode(normalizedSearchTerm); - if (decoded && decoded.type === "npub") { - return { - filter: { - kinds: [0], - authors: [decoded.data], - limit: 1, // AI-NOTE: 2025-01-08 - Only need 1 result for specific npub search - }, - subscriptionType: "npub-specific", - }; - } - } catch { - // Not a valid npub, continue with other strategies - } - // Try NIP-05 lookup first - try { - for (const domain of COMMON_DOMAINS) { - const nip05Address = `${normalizedSearchTerm}@${domain}`; - try { - const npub = await getNpubFromNip05(nip05Address); - if (npub) { - return { - filter: { - kinds: [0], - authors: [npub], - limit: 1, // AI-NOTE: 2025-01-08 - Only need 1 result for specific npub search - }, - subscriptionType: "nip05-found", - }; - } - } catch { - // Continue to next domain - } - } - } catch { - // Fallback to reasonable profile search - } - - return { - filter: { kinds: [0], limit: SEARCH_LIMITS.GENERAL_PROFILE }, - subscriptionType: "profile", - }; -} /** * Create primary relay set for search operations @@ -562,10 +618,11 @@ function processProfileEvent( ) { if (!event.content) return; - // If this is a specific npub search or NIP-05 found search, include all matching events + // If this is a specific npub search, NIP-05 found search, or profile-search, include all matching events if ( subscriptionType === "npub-specific" || - subscriptionType === "nip05-found" + subscriptionType === "nip05-found" || + subscriptionType === "profile-search" ) { searchState.foundProfiles.push(event); return; diff --git a/src/lib/utils/tag_event_fetch.ts b/src/lib/utils/tag_event_fetch.ts index 42b5dfa..651ba80 100644 --- a/src/lib/utils/tag_event_fetch.ts +++ b/src/lib/utils/tag_event_fetch.ts @@ -1,5 +1,5 @@ import NDK, { type NDKEvent } from "@nostr-dev-kit/ndk"; -import { batchFetchProfiles, extractPubkeysFromEvents } from "./profileCache.ts"; +import { batchFetchProfiles, extractPubkeysFromEvents } from "./npubCache.ts"; // Constants for publication event kinds const INDEX_EVENT_KIND = 30040; diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index 96e5c56..1a369da 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -11,14 +11,17 @@ import { userBadge } from "$lib/snippets/UserSnippets.svelte"; import { getMatchingTags, toNpub, getUserMetadata } from "$lib/utils/nostrUtils"; import EventInput from "$lib/components/EventInput.svelte"; - import { userPubkey, isLoggedIn } from "$lib/stores/authStore.Svelte"; import CopyToClipboard from "$lib/components/util/CopyToClipboard.svelte"; import { neventEncode, naddrEncode } from "$lib/utils"; - import { activeInboxRelays } from "$lib/ndk"; + import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; import { getEventType } from "$lib/utils/mime"; import ViewPublicationLink from "$lib/components/util/ViewPublicationLink.svelte"; import { checkCommunity } from "$lib/utils/search_utility"; import EmbeddedEvent from "$lib/components/embedded_events/EmbeddedEvent.svelte"; + import { userStore } from "$lib/stores/userStore"; + import { fetchCurrentUserLists, isPubkeyInUserLists } from "$lib/utils/user_lists"; + import { UserOutline } from "flowbite-svelte-icons"; + import { clearAllCaches, clearProfileCaches, getCacheStats } from "$lib/utils/cache_manager"; let loading = $state(false); let error = $state(null); @@ -41,6 +44,8 @@ website?: string; lud16?: string; nip05?: string; + isInUserLists?: boolean; + listKinds?: number[]; } | null>(null); let userRelayPreference = $state(false); let showSidePanel = $state(false); @@ -48,16 +53,52 @@ let secondOrderSearchMessage = $state(null); let communityStatus = $state>({}); let searchResultsCollapsed = $state(false); + let user = $state(null); + + userStore.subscribe((val) => (user = val)); + + // Debug: Check if user is logged in + $effect(() => { + console.log("[Events Page] User state:", user); + console.log("[Events Page] User signed in:", user?.signedIn); + console.log("[Events Page] User pubkey:", user?.pubkey); + }); function handleEventFound(newEvent: NDKEvent) { event = newEvent; showSidePanel = true; - // AI-NOTE: 2025-01-24 - Preserve search results to allow navigation through them - // Don't clear search results when showing a single event - this allows users to browse through results + // searchInProgress = false; + // secondOrderSearchMessage = null; + + // AI-NOTE: 2025-01-24 - Properly parse profile data for kind 0 events if (newEvent.kind === 0) { try { - profile = JSON.parse(newEvent.content); - } catch { + const parsedProfile = parseProfileContent(newEvent); + if (parsedProfile) { + profile = parsedProfile; + console.log("[Events Page] Parsed profile data:", parsedProfile); + + // If the event doesn't have user list information, fetch it + if (typeof parsedProfile.isInUserLists !== 'boolean') { + fetchCurrentUserLists() + .then((userLists) => { + const isInLists = isPubkeyInUserLists(newEvent.pubkey, userLists); + // Update the profile with user list information + profile = { ...parsedProfile, isInUserLists: isInLists } as any; + // Also update the event's profileData + (newEvent as any).profileData = { ...parsedProfile, isInUserLists: isInLists }; + }) + .catch(() => { + profile = { ...parsedProfile, isInUserLists: false } as any; + (newEvent as any).profileData = { ...parsedProfile, isInUserLists: false }; + }); + } + } else { + console.warn("[Events Page] Failed to parse profile content for event:", newEvent.id); + profile = null; + } + } catch (error) { + console.error("[Events Page] Error parsing profile content:", error); profile = null; } } else { @@ -67,6 +108,20 @@ // AI-NOTE: 2025-01-24 - Ensure profile is cached for the event author if (newEvent.pubkey) { cacheProfileForPubkey(newEvent.pubkey); + + // Update profile data with user list information + updateProfileDataWithUserLists([newEvent]); + + // Also check community status for the individual event + if (!communityStatus[newEvent.pubkey]) { + checkCommunity(newEvent.pubkey) + .then((status) => { + communityStatus = { ...communityStatus, [newEvent.pubkey]: status }; + }) + .catch(() => { + communityStatus = { ...communityStatus, [newEvent.pubkey]: false }; + }); + } } } @@ -84,6 +139,26 @@ } } + // AI-NOTE: 2025-01-24 - Function to update profile data with user list information + async function updateProfileDataWithUserLists(events: NDKEvent[]) { + try { + const userLists = await fetchCurrentUserLists(); + + for (const event of events) { + if (event.kind === 0 && event.pubkey) { + const existingProfileData = (event as any).profileData || parseProfileContent(event); + + if (existingProfileData) { + const isInLists = isPubkeyInUserLists(event.pubkey, userLists); + (event as any).profileData = { ...existingProfileData, isInUserLists: isInLists }; + } + } + } + } catch (error) { + console.warn("[Events Page] Failed to update profile data with user lists:", error); + } + } + // Use Svelte 5 idiomatic effect to update searchValue when $page.url.searchParams.get('id') changes $effect(() => { const url = $page.url.searchParams; @@ -137,6 +212,21 @@ } }); + // AI-NOTE: 2025-01-24 - Function to ensure events have created_at property + // This fixes the "Unknown date" issue when events are retrieved from cache + function ensureEventProperties(events: NDKEvent[]): NDKEvent[] { + return events.map(event => { + if (event && typeof event === 'object') { + // Ensure created_at is set + if (!event.created_at && event.created_at !== 0) { + console.warn("[Events Page] Event missing created_at, setting to 0:", event.id); + (event as any).created_at = 0; + } + } + return event; + }); + } + function handleSearchResults( results: NDKEvent[], secondOrder: NDKEvent[] = [], @@ -146,9 +236,14 @@ searchTypeParam?: string, searchTermParam?: string, ) { - searchResults = results; - secondOrderResults = secondOrder; - tTagResults = tTagEvents; + // AI-NOTE: 2025-01-24 - Ensure all events have proper properties + const processedResults = ensureEventProperties(results); + const processedSecondOrder = ensureEventProperties(secondOrder); + const processedTTagEvents = ensureEventProperties(tTagEvents); + + searchResults = processedResults; + secondOrderResults = processedSecondOrder; + tTagResults = processedTTagEvents; originalEventIds = eventIds; originalAddresses = addresses; searchType = searchTypeParam || null; @@ -211,6 +306,9 @@ const cachePromises = Array.from(uniquePubkeys).map(pubkey => cacheProfileForPubkey(pubkey)); await Promise.allSettled(cachePromises); + // AI-NOTE: 2025-01-24 - Update profile data with user list information for cached events + await updateProfileDataWithUserLists(events); + console.log(`[Events Page] Profile caching complete`); } @@ -231,6 +329,18 @@ goto("/events", { replaceState: true }); } + function handleClearCache() { + clearAllCaches(); + // Force a page refresh to ensure all caches are cleared + window.location.reload(); + } + + function handleClearProfileCache() { + clearProfileCaches(); + // Force a page refresh to ensure profile caches are cleared + window.location.reload(); + } + function closeSidePanel() { showSidePanel = false; event = null; @@ -291,6 +401,8 @@ website?: string; lud16?: string; nip05?: string; + isInUserLists?: boolean; + listKinds?: number[]; } | null { if (event.kind !== 0 || !event.content) { return null; @@ -342,6 +454,16 @@ return addr.slice(0, head) + "…" + addr.slice(-tail); } + function formatEventDate(event: NDKEvent): string { + if (event.created_at) { + return new Date(event.created_at * 1000).toLocaleDateString(); + } + if ((event as any).timestamp) { + return new Date((event as any).timestamp * 1000).toLocaleDateString(); + } + return "Unknown date"; + } + function onLoadingChange(val: boolean) { loading = val; searchInProgress = @@ -384,6 +506,13 @@

Events
+ {#if showSidePanel && (searchResults.length > 0 || secondOrderResults.length > 0 || tTagResults.length > 0)}
- {#if isLoggedIn && userPubkey} + {#if user?.signedIn}
Add Comment diff --git a/src/routes/visualize/+page.svelte b/src/routes/visualize/+page.svelte index fa46745..50dd4dd 100644 --- a/src/routes/visualize/+page.svelte +++ b/src/routes/visualize/+page.svelte @@ -15,8 +15,9 @@ import { filterByDisplayLimits, detectMissingEvents, buildCoordinateMap } from "$lib/utils/displayLimits"; import type { PageData } from './$types'; import { getEventKindColor, getEventKindName } from "$lib/utils/eventColors"; - import { extractPubkeysFromEvents, batchFetchProfiles } from "$lib/utils/profileCache"; - import { activePubkey, getNdkContext } from "$lib/ndk"; + import { extractPubkeysFromEvents, batchFetchProfiles } from "$lib/utils/npubCache"; + import { userStore } from "$lib/stores/userStore"; + import { getNdkContext } from "$lib/ndk"; // Import utility functions for tag-based event fetching // These functions handle the complex logic of finding publications by tags // and extracting their associated content events @@ -123,7 +124,7 @@ } // Get the current user's pubkey - const currentUserPubkey = get(activePubkey); + const currentUserPubkey = get(userStore).pubkey; if (!currentUserPubkey) { console.warn("No logged-in user, cannot fetch user's follow list"); return [];