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
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 @@
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;
+ }
}