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)}