Browse Source

fixed profile search

master
silberengel 7 months ago
parent
commit
f310320c6e
  1. 10
      src/lib/components/EventInput.svelte
  2. 11
      src/lib/components/cards/ProfileHeader.svelte
  3. 12
      src/lib/components/embedded_events/EmbeddedEvent.svelte
  4. 2
      src/lib/navigator/EventNetwork/NodeTooltip.svelte
  5. 2
      src/lib/navigator/EventNetwork/utils/networkBuilder.ts
  6. 2
      src/lib/navigator/EventNetwork/utils/personNetworkBuilder.ts
  7. 2
      src/lib/navigator/EventNetwork/utils/tagNetworkBuilder.ts
  8. 4
      src/lib/ndk.ts
  9. 11
      src/lib/stores/authStore.Svelte.ts
  10. 6
      src/lib/stores/userStore.ts
  11. 85
      src/lib/utils/cache_manager.ts
  12. 85
      src/lib/utils/nostrUtils.ts
  13. 375
      src/lib/utils/npubCache.ts
  14. 257
      src/lib/utils/profileCache.ts
  15. 99
      src/lib/utils/profile_search.ts
  16. 2
      src/lib/utils/search_types.ts
  17. 171
      src/lib/utils/subscription_search.ts
  18. 2
      src/lib/utils/tag_event_fetch.ts
  19. 180
      src/routes/events/+page.svelte
  20. 7
      src/routes/visualize/+page.svelte

10
src/lib/components/EventInput.svelte

@ -18,7 +18,7 @@ @@ -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 @@ @@ -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 @@ @@ -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;

11
src/lib/components/cards/ProfileHeader.svelte

@ -16,6 +16,7 @@ @@ -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 @@ @@ -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');
}}
/>
<div class="w-16 h-16 rounded-full border flex-shrink-0 bg-gray-300 dark:bg-gray-600 flex items-center justify-center hidden">
<UserOutline class="w-8 h-8 text-gray-600 dark:text-gray-300" />
</div>
{:else}
<div class="w-16 h-16 rounded-full border flex-shrink-0 bg-gray-300 dark:bg-gray-600 flex items-center justify-center">
<UserOutline class="w-8 h-8 text-gray-600 dark:text-gray-300" />
</div>
{/if}
<div class="flex items-center gap-2 min-w-0 flex-1">
<div class="min-w-0 flex-1">

12
src/lib/components/embedded_events/EmbeddedEvent.svelte

@ -9,6 +9,7 @@ @@ -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 @@ @@ -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');
}}
/>
<div class="w-12 h-12 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center flex-shrink-0 hidden">
<UserOutline class="w-6 h-6 text-gray-600 dark:text-gray-300" />
</div>
{:else}
<div class="w-12 h-12 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center flex-shrink-0">
<UserOutline class="w-6 h-6 text-gray-600 dark:text-gray-300" />
</div>
{/if}
{#if profile.about}
<p class="text-sm text-gray-700 dark:text-gray-300 break-words">

2
src/lib/navigator/EventNetwork/NodeTooltip.svelte

@ -12,7 +12,7 @@ @@ -12,7 +12,7 @@
import {
getDisplayNameSync,
replacePubkeysWithDisplayNames,
} from "$lib/utils/profileCache";
} from "$lib/utils/npubCache";
import {indexKind, zettelKinds, wikiKind} from "$lib/consts";
// Component props

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

@ -10,7 +10,7 @@ import type { GraphData, GraphState, NetworkLink, NetworkNode } from "../types"; @@ -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

2
src/lib/navigator/EventNetwork/utils/personNetworkBuilder.ts

@ -6,7 +6,7 @@ @@ -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;

2
src/lib/navigator/EventNetwork/utils/tagNetworkBuilder.ts

@ -7,7 +7,7 @@ @@ -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

4
src/lib/ndk.ts

@ -14,7 +14,7 @@ import { @@ -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( @@ -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 { @@ -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

11
src/lib/stores/authStore.Svelte.ts

@ -1,11 +0,0 @@ @@ -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<string | null>(null);
/**
* Derived store indicating if the user is logged in.
*/
export const isLoggedIn = derived(userPubkey, ($userPubkey) => !!$userPubkey);

6
src/lib/stores/userStore.ts

@ -15,7 +15,7 @@ import { @@ -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) { @@ -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: @@ -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) { @@ -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) { @@ -470,7 +467,6 @@ export function logoutUser(ndk: NDK) {
signer: null,
signedIn: false,
});
userPubkey.set(null);
if (ndk) {
ndk.activeUser = undefined;

85
src/lib/utils/cache_manager.ts

@ -0,0 +1,85 @@ @@ -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(),
};
}

85
src/lib/utils/nostrUtils.ts

@ -1,6 +1,6 @@ @@ -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( @@ -70,87 +70,8 @@ export async function getUserMetadata(
ndk?: NDK,
force = false,
): Promise<NostrProfile> {
// 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);
}
/**

375
src/lib/utils/npubCache.ts

@ -1,11 +1,21 @@ @@ -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<string, NpubMetadata> = {};
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<string, CacheEntry> = 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 { @@ -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<string, CacheEntry>;
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<string, CacheEntry> = {};
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<NpubMetadata> {
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<NpubMetadata> {
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<string, NpubMetadata> {
return { ...this.cache };
const result: Record<string, NpubMetadata> = {};
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<string> {
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<NDKEvent[]> {
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<string> {
const pubkeys = new Set<string>();
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);
});
}

257
src/lib/utils/profileCache.ts

@ -1,257 +0,0 @@ @@ -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<string, ProfileData>();
/**
* 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<ProfileData | null> {
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<string> {
// 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<NDKEvent[]> {
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<string> {
const pubkeys = new Set<string>();
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);
});
}

99
src/lib/utils/profile_search.ts

@ -1,8 +1,8 @@ @@ -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 { @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -346,6 +420,7 @@ async function quickRelaySearch(
nip05: profileData.nip05,
pubkey: event.pubkey,
searchTerm: normalizedSearchTerm,
relay: uniqueRelayUrls[index],
});
const profile = createProfileFromEvent(event, profileData);

2
src/lib/utils/search_types.ts

@ -64,6 +64,8 @@ export type SearchSubscriptionType = "d" | "t" | "n"; @@ -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
}
/**

171
src/lib/utils/subscription_search.ts

@ -59,15 +59,49 @@ export async function searchBySubscription( @@ -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( @@ -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( @@ -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) { @@ -338,6 +398,7 @@ function createCleanupFunction(searchState: any) {
async function createSearchFilter(
searchType: SearchSubscriptionType,
normalizedSearchTerm: string,
ndk: NDK,
): Promise<SearchFilter> {
console.log("subscription_search: Creating search filter for:", {
searchType,
@ -368,8 +429,54 @@ async function createSearchFilter( @@ -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( @@ -378,58 +485,7 @@ async function createSearchFilter(
}
}
/**
* Create profile search filter
*/
async function createProfileSearchFilter(
normalizedSearchTerm: string,
): Promise<SearchFilter> {
// 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( @@ -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;

2
src/lib/utils/tag_event_fetch.ts

@ -1,5 +1,5 @@ @@ -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;

180
src/routes/events/+page.svelte

@ -11,14 +11,17 @@ @@ -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<string | null>(null);
@ -41,6 +44,8 @@ @@ -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 @@ @@ -48,16 +53,52 @@
let secondOrderSearchMessage = $state<string | null>(null);
let communityStatus = $state<Record<string, boolean>>({});
let searchResultsCollapsed = $state(false);
let user = $state<any>(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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -384,6 +506,13 @@
<div class="flex justify-between items-center">
<Heading tag="h1" class="h-leather mb-2">Events</Heading>
<div class="flex items-center gap-2">
<button
class="text-sm text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-200 border border-red-300 dark:border-red-600 rounded px-2 py-1"
onclick={handleClearCache}
title="Clear all caches to refresh stale data"
>
Clear Cache
</button>
{#if showSidePanel && (searchResults.length > 0 || secondOrderResults.length > 0 || tTagResults.length > 0)}
<button
class="lg:hidden text-sm text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 border border-gray-300 dark:border-gray-600 rounded px-2 py-1"
@ -447,7 +576,7 @@ @@ -447,7 +576,7 @@
</Heading>
<div class="space-y-4">
{#each searchResults as result, index}
{@const profileData = parseProfileContent(result)}
{@const profileData = (result as any).profileData || parseProfileContent(result)}
<button
class="w-full text-left border border-gray-300 dark:border-gray-600 rounded-lg p-4 bg-white dark:bg-primary-900/70 hover:bg-gray-100 dark:hover:bg-primary-800 focus:bg-gray-100 dark:focus:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-primary-500 transition-colors overflow-hidden"
onclick={() => handleEventFound(result)}
@ -461,7 +590,22 @@ @@ -461,7 +590,22 @@
<span class="text-xs text-gray-600 dark:text-gray-400"
>Kind: {result.kind}</span
>
{#if result.pubkey && communityStatus[result.pubkey]}
{#if profileData?.isInUserLists}
<div
class="flex-shrink-0 w-4 h-4 bg-red-100 dark:bg-red-900 rounded-full flex items-center justify-center"
title="In your lists (follows, etc.)"
>
<svg
class="w-3 h-3 text-red-600 dark:text-red-400"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"
/>
</svg>
</div>
{:else if result.pubkey && communityStatus[result.pubkey]}
<div
class="flex-shrink-0 w-4 h-4 bg-yellow-100 dark:bg-yellow-900 rounded-full flex items-center justify-center"
title="Has posted to the community"
@ -504,13 +648,15 @@ @@ -504,13 +648,15 @@
class="w-12 h-12 rounded-full object-cover border border-gray-200 dark:border-gray-600"
onerror={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
(e.target as HTMLImageElement).nextElementSibling?.classList.remove('hidden');
}}
/>
<div class="w-12 h-12 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center border border-gray-200 dark:border-gray-600 hidden">
<UserOutline class="w-6 h-6 text-gray-600 dark:text-gray-300" />
</div>
{:else}
<div class="w-12 h-12 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center border border-gray-200 dark:border-gray-600">
<span class="text-lg font-medium text-gray-600 dark:text-gray-300">
{(profileData.display_name || profileData.name || result.pubkey.slice(0, 1)).toUpperCase()}
</span>
<UserOutline class="w-6 h-6 text-gray-600 dark:text-gray-300" />
</div>
{/if}
<div class="flex flex-col min-w-0 flex-1">
@ -604,7 +750,7 @@ @@ -604,7 +750,7 @@
</P>
<div class="space-y-4">
{#each secondOrderResults as result, index}
{@const profileData = parseProfileContent(result)}
{@const profileData = (result as any).profileData || parseProfileContent(result)}
<button
class="w-full text-left border border-gray-300 dark:border-gray-600 rounded-lg p-4 bg-gray-50 dark:bg-primary-800/50 hover:bg-gray-100 dark:hover:bg-primary-700 focus:bg-gray-100 dark:focus:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 transition-colors overflow-hidden"
onclick={() => handleEventFound(result)}
@ -761,7 +907,7 @@ @@ -761,7 +907,7 @@
</P>
<div class="space-y-4">
{#each tTagResults as result, index}
{@const profileData = parseProfileContent(result)}
{@const profileData = (result as any).profileData || parseProfileContent(result)}
<button
class="w-full text-left border border-gray-300 dark:border-gray-600 rounded-lg p-4 bg-gray-50 dark:bg-primary-800/50 hover:bg-gray-100 dark:hover:bg-primary-700 focus:bg-gray-100 dark:focus:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 transition-colors overflow-hidden"
onclick={() => handleEventFound(result)}
@ -952,7 +1098,7 @@ @@ -952,7 +1098,7 @@
<CommentViewer {event} />
</div>
{#if isLoggedIn && userPubkey}
{#if user?.signedIn}
<div class="mt-8 min-w-0 overflow-hidden">
<Heading tag="h3" class="h-leather mb-4 break-words">Add Comment</Heading>
<CommentBox {event} {userRelayPreference} />

7
src/routes/visualize/+page.svelte

@ -15,8 +15,9 @@ @@ -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 @@ @@ -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 [];

Loading…
Cancel
Save