diff --git a/src/app/components/explore/explore.component.html b/src/app/components/explore/explore.component.html index 782f68b..cf6571b 100644 --- a/src/app/components/explore/explore.component.html +++ b/src/app/components/explore/explore.component.html @@ -50,6 +50,7 @@

Explore Projects

Card cover image diff --git a/src/app/components/explore/explore.component.ts b/src/app/components/explore/explore.component.ts index a5f24d8..045db3b 100644 --- a/src/app/components/explore/explore.component.ts +++ b/src/app/components/explore/explore.component.ts @@ -69,7 +69,6 @@ export class ExploreComponent implements OnInit, OnDestroy { this.projects.forEach(project => this.subscribeToProjectMetadata(project)); } - this._indexedDBService.getMetadataStream() .pipe(takeUntil(this._unsubscribeAll)) .subscribe((updatedMetadata) => { @@ -96,15 +95,11 @@ export class ExploreComponent implements OnInit, OnDestroy { this.projects = [...this.projects, ...projects]; this.filteredProjects = [...this.projects]; - - for (let i = 0; i < projects.length; i += this.metadataLoadLimit) { - const batch = projects.slice(i, i + this.metadataLoadLimit); - await Promise.all(batch.map(project => this.loadMetadataForProject(project))); - } + const pubkeys = projects.map(project => project.nostrPubKey); + await this.metadataService.fetchMetadataForMultipleKeys(pubkeys); this.stateService.setProjects(this.projects); - this.projects.forEach(project => this.subscribeToProjectMetadata(project)); } this.loading = false; @@ -117,6 +112,7 @@ export class ExploreComponent implements OnInit, OnDestroy { }); } + async loadMetadataForProject(project: Project): Promise { try { const metadata = await this.metadataService.fetchMetadataWithCache(project.nostrPubKey); @@ -136,13 +132,13 @@ export class ExploreComponent implements OnInit, OnDestroy { displayName: metadata.name, about: metadata.about, picture: metadata.picture, - banner:metadata.banner + banner: metadata.banner }; const index = this.projects.findIndex(p => p.projectIdentifier === project.projectIdentifier); if (index !== -1) { this.projects[index] = updatedProject; - this.projects = [...this.projects]; + this.projects = [...this.projects]; } this.filteredProjects = [...this.projects]; @@ -154,7 +150,7 @@ export class ExploreComponent implements OnInit, OnDestroy { .pipe(takeUntil(this._unsubscribeAll)) .subscribe((updatedMetadata: any) => { if (updatedMetadata && updatedMetadata.pubkey === project.nostrPubKey) { - this.updateProjectMetadata(project, updatedMetadata); + this.updateProjectMetadata(project, updatedMetadata.metadata); } }); } diff --git a/src/app/components/profile/profile.component.html b/src/app/components/profile/profile.component.html index e4a0a0a..62b8f4b 100644 --- a/src/app/components/profile/profile.component.html +++ b/src/app/components/profile/profile.component.html @@ -5,8 +5,8 @@
Cover image
diff --git a/src/app/services/indexed-db.service.ts b/src/app/services/indexed-db.service.ts index 6d8ced5..63f525e 100644 --- a/src/app/services/indexed-db.service.ts +++ b/src/app/services/indexed-db.service.ts @@ -36,7 +36,6 @@ export class IndexedDBService { async saveUserMetadata(pubkey: string, metadata: any): Promise { try { await localForage.setItem(pubkey, metadata); - console.log('Metadata saved successfully!'); this.metadataSubject.next({ pubkey, metadata }); } catch (error) { console.error('Error saving metadata to IndexedDB:', error); @@ -46,7 +45,6 @@ export class IndexedDBService { async removeUserMetadata(pubkey: string): Promise { try { await localForage.removeItem(pubkey); - console.log(`Metadata for pubkey ${pubkey} removed successfully!`); this.metadataSubject.next({ pubkey, metadata: null }); } catch (error) { console.error('Error removing metadata from IndexedDB:', error); @@ -56,7 +54,6 @@ export class IndexedDBService { async clearAllMetadata(): Promise { try { await localForage.clear(); - console.log('All metadata cleared successfully!'); this.metadataSubject.next(null); } catch (error) { console.error('Error clearing all metadata:', error); diff --git a/src/app/services/metadata.service.ts b/src/app/services/metadata.service.ts index 3ea3516..5c685dc 100644 --- a/src/app/services/metadata.service.ts +++ b/src/app/services/metadata.service.ts @@ -22,7 +22,7 @@ export class MetadataService { getMetadataStream(): Observable { - return this.metadataSubject.asObservable(); + return this.metadataSubject.asObservable().pipe(throttleTime(2000)); } @@ -31,6 +31,44 @@ export class MetadataService { this.processQueue(); } + async fetchMetadataForMultipleKeys(pubkeys: string[]): Promise { + const filter: Filter = { + kinds: [0], + authors: pubkeys, + }; + + try { + await this.relayService.ensureConnectedRelays(); + const connectedRelays = this.relayService.getConnectedRelays(); + + if (connectedRelays.length === 0) { + console.error('No relays are connected.'); + return; + } + + const sub = this.relayService.getPool().subscribeMany(connectedRelays, [filter], { + onevent: async (event: NostrEvent) => { + if (event.kind === 0) { + try { + const metadata = JSON.parse(event.content); + await this.indexedDBService.saveUserMetadata(event.pubkey, metadata); + this.metadataSubject.next({ pubkey: event.pubkey, metadata }); + } catch (error) { + console.error('Error parsing metadata:', error); + } + } + }, + oneose: () => { + } + }); + + setTimeout(() => { + sub.close(); + }, 10 * 60 * 1000); + } catch (error) { + console.error('Failed to fetch metadata for multiple keys:', error); + } + } private async processQueue(): Promise { if (this.isProcessingQueue || this.requestQueue.size === 0) { @@ -43,33 +81,28 @@ export class MetadataService { const batch = Array.from(this.requestQueue).slice(0, this.maxRequestsPerBatch); this.requestQueue = new Set(Array.from(this.requestQueue).slice(this.maxRequestsPerBatch)); - await Promise.all(batch.map(async (pubkey) => { try { const updatedMetadata = await this.fetchMetadataRealtime(pubkey); if (updatedMetadata) { await this.indexedDBService.saveUserMetadata(pubkey, updatedMetadata); this.metadataSubject.next(updatedMetadata); - console.log(`Metadata updated for user: ${pubkey}`); - } + } } catch (error) { console.error(`Failed to update metadata for user: ${pubkey}`, error); } })); - await new Promise(resolve => setTimeout(resolve, this.requestDelay)); } this.isProcessingQueue = false; } - async fetchMetadataWithCache(pubkey: string): Promise { const metadata = await this.indexedDBService.getUserMetadata(pubkey); if (metadata) { this.metadataSubject.next(metadata); - console.log('Metadata loaded from IndexedDB'); } else { this.enqueueRequest(pubkey); } @@ -78,7 +111,6 @@ export class MetadataService { return metadata; } - private subscribeToMetadataUpdates(pubkey: string): void { this.relayService.ensureConnectedRelays().then(() => { const filter: Filter = { authors: [pubkey], kinds: [0] }; @@ -90,20 +122,19 @@ export class MetadataService { const updatedMetadata = JSON.parse(event.content); await this.indexedDBService.saveUserMetadata(pubkey, updatedMetadata); this.metadataSubject.next(updatedMetadata); - console.log('Real-time metadata update saved to IndexedDB'); - } catch (error) { + } catch (error) { console.error('Error parsing updated metadata:', error); } } }, - oneose() { - console.log('Real-time metadata subscription closed.'); - }, + oneose(){}, }); }); } + + async fetchMetadataRealtime(pubkey: string): Promise { await this.relayService.ensureConnectedRelays(); const connectedRelays = this.relayService.getConnectedRelays(); diff --git a/src/app/services/projects.service.ts b/src/app/services/projects.service.ts index 92178b2..4d99897 100644 --- a/src/app/services/projects.service.ts +++ b/src/app/services/projects.service.ts @@ -26,7 +26,7 @@ export interface ProjectStats { }) export class ProjectsService { private offset = 0; - private limit = 9; + private limit = 45; private totalProjects = 0; private loading = false; private projects: Project[] = []; @@ -56,8 +56,6 @@ export class ProjectsService { ? `${indexerUrl}api/query/Angor/projects?offset=${this.offset}&limit=${this.limit}` : `${indexerUrl}api/query/Angor/projects?limit=${this.limit}`; - console.log(`Fetching projects from URL: ${url}`); - try { const response = await this.http .get(url, { observe: 'response' }) @@ -66,14 +64,12 @@ export class ProjectsService { if (!this.totalProjectsFetched && response && response.headers) { const paginationTotal = response.headers.get('pagination-total'); this.totalProjects = paginationTotal ? +paginationTotal : 0; - console.log(`Total projects: ${this.totalProjects}`); this.totalProjectsFetched = true; this.offset = Math.max(this.totalProjects - this.limit, 0); } const newProjects = response?.body || []; - console.log('New projects received:', newProjects); if (!newProjects || newProjects.length === 0) { this.noMoreProjects = true; @@ -89,8 +85,6 @@ export class ProjectsService { if (uniqueNewProjects.length > 0) { this.projects = [...this.projects, ...uniqueNewProjects]; - console.log(`${uniqueNewProjects.length} new projects added`); - this.offset = Math.max(this.offset - this.limit, 0); return uniqueNewProjects; } else { @@ -110,8 +104,6 @@ export class ProjectsService { fetchProjectStats(projectIdentifier: string): Observable { const indexerUrl = this.indexerService.getPrimaryIndexer(this.selectedNetwork); const url = `${indexerUrl}api/query/Angor/projects/${projectIdentifier}/stats`; - console.log(`Fetching project stats from URL: ${url}`); - return this.http.get(url).pipe( catchError((error) => { console.error( @@ -126,8 +118,6 @@ export class ProjectsService { fetchProjectDetails(projectIdentifier: string): Observable { const indexerUrl = this.indexerService.getPrimaryIndexer(this.selectedNetwork); const url = `${indexerUrl}api/query/Angor/projects/${projectIdentifier}`; - console.log(`Fetching project details from URL: ${url}`); - return this.http.get(url).pipe( catchError((error) => { console.error( diff --git a/src/app/services/relay.service.ts b/src/app/services/relay.service.ts index 2a51198..c9a8d74 100644 --- a/src/app/services/relay.service.ts +++ b/src/app/services/relay.service.ts @@ -1,181 +1,216 @@ import { Injectable } from "@angular/core"; import { Filter, NostrEvent, SimplePool } from "nostr-tools"; -import { Observable, Subject } from "rxjs"; +import { Observable, Subject, throttleTime } from "rxjs"; @Injectable({ - providedIn: 'root', + providedIn: 'root', }) export class RelayService { - private pool: SimplePool; - private relays: { url: string, connected: boolean, retries: number, retryTimeout: any, ws?: WebSocket }[] = []; - private maxRetries = 10; - private retryDelay = 15000; - private eventSubject = new Subject(); - - constructor() { - this.pool = new SimplePool(); - this.relays = this.loadRelaysFromLocalStorage(); - this.connectToRelays(); - this.setupVisibilityChangeHandling(); - } - - private loadRelaysFromLocalStorage() { - const defaultRelays = [ - { url: 'wss://relay.angor.io', connected: false, retries: 0, retryTimeout: null, ws: undefined }, - { url: 'wss://relay2.angor.io', connected: false, retries: 0, retryTimeout: null, ws: undefined }, - ]; - - const storedRelays = JSON.parse(localStorage.getItem('nostrRelays') || '[]').map((relay: any) => ({ - ...relay, - connected: false, - retries: 0, - retryTimeout: null, - ws: undefined, - })); - return [...defaultRelays, ...storedRelays]; - } - - private connectToRelay(relay: { url: string, connected: boolean, retries: number, retryTimeout: any, ws?: WebSocket }) { - if (relay.connected) { - return; - } - - relay.ws = new WebSocket(relay.url); - - relay.ws.onopen = () => { - relay.connected = true; - relay.retries = 0; - clearTimeout(relay.retryTimeout); - console.log(`Connected to relay: ${relay.url}`); - this.saveRelaysToLocalStorage(); - }; - - relay.ws.onerror = (error) => { - console.error(`Failed to connect to relay: ${relay.url}`, error); - this.handleRelayError(relay); - }; - - relay.ws.onclose = () => { - relay.connected = false; - console.log(`Disconnected from relay: ${relay.url}`); - this.handleRelayError(relay); - }; - - relay.ws.onmessage = (message) => { - try { - const dataStr = typeof message.data === 'string' ? message.data : message.data.toString('utf-8'); - const parsedData = JSON.parse(dataStr); - this.eventSubject.next(parsedData); - } catch (error) { - console.error('Error parsing WebSocket message:', error); - } - }; - } - - private handleRelayError(relay: { url: string, connected: boolean, retries: number, retryTimeout: any, ws?: WebSocket }) { - if (relay.retries >= this.maxRetries) { - console.error(`Max retries reached for relay: ${relay.url}. No further attempts will be made.`); - return; - } - - const retryInterval = this.retryDelay * relay.retries; - relay.retries++; - - relay.retryTimeout = setTimeout(() => { - this.connectToRelay(relay); - console.log(`Retrying connection to relay: ${relay.url} (Attempt ${relay.retries})`); - }, retryInterval); - } - - public connectToRelays() { - this.relays.forEach((relay) => { - if (!relay.connected) { - this.connectToRelay(relay); - } - }); - } - - public async ensureConnectedRelays(): Promise { - this.connectToRelays(); - - return new Promise((resolve) => { - const checkConnection = () => { - if (this.getConnectedRelays().length > 0) { - resolve(); - } else { - setTimeout(checkConnection, 1000); + private pool: SimplePool; + private relays: { url: string, connected: boolean, retries: number, retryTimeout: any, ws?: WebSocket }[] = []; + private maxRetries = 10; + private retryDelay = 15000; + private eventSubject = new Subject(); + private requestQueue: Set = new Set(); + private isProcessingQueue = false; + private maxConcurrentRequests = 2; + + constructor() { + this.pool = new SimplePool(); + this.relays = this.loadRelaysFromLocalStorage(); + this.connectToRelays(); + this.setupVisibilityChangeHandling(); + } + + private loadRelaysFromLocalStorage() { + const defaultRelays = [ + { url: 'wss://relay.angor.io', connected: false, retries: 0, retryTimeout: null, ws: undefined }, + { url: 'wss://relay2.angor.io', connected: false, retries: 0, retryTimeout: null, ws: undefined }, + ]; + + const storedRelays = JSON.parse(localStorage.getItem('nostrRelays') || '[]').map((relay: any) => ({ + ...relay, + connected: false, + retries: 0, + retryTimeout: null, + ws: undefined, + })); + return [...defaultRelays, ...storedRelays]; + } + + private connectToRelay(relay: { url: string, connected: boolean, retries: number, retryTimeout: any, ws?: WebSocket }) { + if (relay.connected) { + return; + } + + relay.ws = new WebSocket(relay.url); + + relay.ws.onopen = () => { + try { + relay.connected = true; + relay.retries = 0; + clearTimeout(relay.retryTimeout); + this.saveRelaysToLocalStorage(); + } catch (error) { + console.error('Error parsing WebSocket message:', error); + } + }; + + relay.ws.onerror = (error) => { + try { + this.handleRelayError(relay); + } catch (error) { + console.error('Error parsing WebSocket message:', error); + } + }; + + relay.ws.onclose = () => { + try { + relay.connected = false; + this.handleRelayError(relay); + } catch (error) { + console.error('Error parsing WebSocket message:', error); + } + }; + + relay.ws.onmessage = (message) => { + try { + const dataStr = typeof message.data === 'string' ? message.data : message.data.toString('utf-8'); + const parsedData = JSON.parse(dataStr); + this.eventSubject.next(parsedData); + } catch (error) { + console.error('Error parsing WebSocket message:', error); + } + }; + } + + private handleRelayError(relay: { url: string, connected: boolean, retries: number, retryTimeout: any, ws?: WebSocket }) { + if (relay.retries >= this.maxRetries) { + console.error(`Max retries reached for relay: ${relay.url}. No further attempts will be made.`); + return; } - }; - checkConnection(); - }); - } + const retryInterval = this.retryDelay * relay.retries; + relay.retries++; + + relay.retryTimeout = setTimeout(() => { + this.connectToRelay(relay); + console.log(`Retrying connection to relay: ${relay.url} (Attempt ${relay.retries})`); + }, retryInterval); + } + + public connectToRelays() { + this.relays.forEach((relay) => { + if (!relay.connected) { + this.connectToRelay(relay); + } + }); + } - private setupVisibilityChangeHandling() { - document.addEventListener('visibilitychange', () => { - if (document.visibilityState === 'visible') { + public async ensureConnectedRelays(): Promise { this.connectToRelays(); - } - }); - window.addEventListener('beforeunload', () => { - this.relays.forEach(relay => { - if (relay.ws) { - relay.ws.close(); + return new Promise((resolve) => { + const checkConnection = () => { + if (this.getConnectedRelays().length > 0) { + resolve(); + } else { + setTimeout(checkConnection, 1000); + } + }; + checkConnection(); + }); + } + + + private setupVisibilityChangeHandling() { + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') { + this.connectToRelays(); + } + }); + + window.addEventListener('beforeunload', () => { + this.relays.forEach(relay => { + if (relay.ws) { + relay.ws.close(); + } + }); + }); + } + + public getConnectedRelays(): string[] { + return this.relays.filter((relay) => relay.connected).map((relay) => relay.url); + } + + public saveRelaysToLocalStorage(): void { + const customRelays = this.relays.filter( + (relay) => !['wss://relay.angor.io', 'wss://relay2.angor.io'].includes(relay.url) + ); + localStorage.setItem('nostrRelays', JSON.stringify(customRelays)); + } + + public getEventStream(): Observable { + return this.eventSubject.asObservable(); + } + + public addRelay(url: string): void { + if (!this.relays.some(relay => relay.url === url)) { + const newRelay = { url, connected: false, retries: 0, retryTimeout: null, ws: undefined }; + this.relays.push(newRelay); + this.connectToRelay(newRelay); + this.saveRelaysToLocalStorage(); } - }); - }); - } - - public getConnectedRelays(): string[] { - return this.relays.filter((relay) => relay.connected).map((relay) => relay.url); - } - - public saveRelaysToLocalStorage(): void { - const customRelays = this.relays.filter( - (relay) => !['wss://relay.angor.io', 'wss://relay2.angor.io'].includes(relay.url) - ); - localStorage.setItem('nostrRelays', JSON.stringify(customRelays)); - } - - public getEventStream(): Observable { - return this.eventSubject.asObservable(); - } - - public addRelay(url: string): void { - if (!this.relays.some(relay => relay.url === url)) { - const newRelay = { url, connected: false, retries: 0, retryTimeout: null, ws: undefined }; - this.relays.push(newRelay); - this.connectToRelay(newRelay); - this.saveRelaysToLocalStorage(); - } - } - - public removeRelay(url: string): void { - this.relays = this.relays.filter(relay => relay.url !== url); - this.saveRelaysToLocalStorage(); - } - - public removeAllCustomRelays(): void { - const defaultRelays = ['wss://relay.angor.io', 'wss://relay2.angor.io']; - this.relays = this.relays.filter(relay => defaultRelays.includes(relay.url)); - this.saveRelaysToLocalStorage(); - } - - public subscribeToFilter(filter: Filter): void { - const connectedRelays = this.getConnectedRelays(); - this.pool.subscribeMany(connectedRelays, [filter], { - onevent: (event: NostrEvent) => { - this.eventSubject.next(event); - }, - }); - } - - public getPool(): SimplePool { - return this.pool; - } - - public getRelays(): { url: string, connected: boolean, ws?: WebSocket }[] { - return this.relays; - } + } + + public removeRelay(url: string): void { + this.relays = this.relays.filter(relay => relay.url !== url); + this.saveRelaysToLocalStorage(); + } + + public removeAllCustomRelays(): void { + const defaultRelays = ['wss://relay.angor.io', 'wss://relay2.angor.io']; + this.relays = this.relays.filter(relay => defaultRelays.includes(relay.url)); + this.saveRelaysToLocalStorage(); + } + + private async processRequestQueue(): Promise { + if (this.isProcessingQueue) { + return; + } + + this.isProcessingQueue = true; + + while (this.requestQueue.size > 0) { + const batch = Array.from(this.requestQueue).slice(0, this.maxConcurrentRequests); + this.requestQueue = new Set(Array.from(this.requestQueue).slice(this.maxConcurrentRequests)); + + await Promise.all(batch.map(async (filterId) => { + console.log(`Processing request for filter: ${filterId}`); + })); + + await new Promise(resolve => setTimeout(resolve, this.retryDelay)); + } + + this.isProcessingQueue = false; + } + + public async subscribeToFilter(filter: Filter): Promise { + try { + this.requestQueue.add(JSON.stringify(filter)); + this.processRequestQueue(); + } catch (error) { + console.error('Failed to subscribe to filter:', error); + } + } + + + + public getPool(): SimplePool { + return this.pool; + } + + public getRelays(): { url: string, connected: boolean, ws?: WebSocket }[] { + return this.relays; + } }