Use multiple relays and improve loading of threads

This commit is contained in:
SondreB 2022-12-29 21:50:39 +01:00
parent fd277af778
commit 4af9f04e03
No known key found for this signature in database
GPG Key ID: D6CC44C75005FDBF
8 changed files with 522 additions and 56 deletions

View File

@ -53,6 +53,16 @@ const routes: Routes = [
component: NoteComponent,
canActivate: [AuthGuard],
},
{
path: 'p/:id',
component: UserComponent,
canActivate: [AuthGuard],
},
{
path: 'e/:id',
component: NoteComponent,
canActivate: [AuthGuard],
},
{
path: 'about',
component: AboutComponent,

View File

@ -1,25 +1,47 @@
<!-- <mat-progress-bar *ngIf="!events || events.length == 0" mode="indeterminate"></mat-progress-bar> -->
<div class="feed-page">
<div class="original-post events noclick" *ngIf="event">
<div class="parent-events events" *ngIf="thread.root$ | async as event">
<span *ngIf="event.kind != 7">
<app-profile-actions [event]="event" [pubkey]="event.pubkey"></app-profile-actions>
<app-profile-header [pubkey]="event.pubkey"
><span class="event-date">{{ event.created_at | ago }}</span> <app-directory-icon [pubkey]="event.pubkey"></app-directory-icon
></app-profile-header>
<div class="content">{{ event.content }}<span *ngIf="event.contentCut">... (message was truncated)</span></div>
</span>
</div>
<!-- <div class="parent-events events" *ngFor="let event of filteredThread(); trackBy: trackByFn">
<span *ngIf="event.kind != 7">
<app-profile-actions [event]="event" [pubkey]="event.pubkey"></app-profile-actions>
<app-profile-header [pubkey]="event.pubkey"
><span class="event-date">{{ event.created_at | ago }}</span> <app-directory-icon [pubkey]="event.pubkey"></app-directory-icon
></app-profile-header>
<div class="content">{{ event.content }}<span *ngIf="event.contentCut">... (message was truncated)</span></div>
</span>
</div> -->
<div class="original-post events noclick" *ngIf="thread.event$ | async as event">
<app-profile-actions [event]="event" [pubkey]="event.pubkey"></app-profile-actions>
<app-profile-header [pubkey]="event.pubkey"
><span class="event-date">{{ event.created_at | ago }}</span> <app-directory-icon [pubkey]="event.pubkey"></app-directory-icon
></app-profile-header>
<div class="content">
{{ event.content }}<span *ngIf="event.contentCut">... (message was truncated)</span>
<span class="dimmed">{{ rootLikes() }} likes&nbsp;&nbsp;{{ rootDislikes() }} dislikes&nbsp;&nbsp;{{ rootReplies() }} replies</span><br>
<br><span class="dimmed">Replying to {{ repliesTo() }}</span>
{{ event.content }}<span *ngIf="event.contentCut">... (message was truncated)</span><br />
<span class="dimmed">{{ rootLikes() }} likes&nbsp;&nbsp;{{ rootDislikes() }} dislikes&nbsp;&nbsp;{{ rootReplies() }} replies</span><br />
<br /><span class="dimmed">Replying to {{ repliesTo() }}</span> <br /><span class="dimmed">Root {{ rootEvent() }}</span>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="page" *ngIf="feedService.thread.length === 0">
<div class="page" *ngIf="!thread.events$">
<h3 class="marginless">Loading thread... this can take some seconds.</h3>
</div>
<div class="feed-page" *ngIf="feedService.thread.length > 0">
<div class="feed-page" *ngIf="thread.events$ | async as events">
<!-- <mat-accordion class="options">
<mat-expansion-panel>
<mat-expansion-panel-header>
@ -35,7 +57,7 @@
</mat-expansion-panel>
</mat-accordion> -->
<div class="events" *ngFor="let event of filteredThread(); trackBy: trackByFn">
<div class="events" *ngFor="let event of events; trackBy: trackByFn">
<span *ngIf="event.kind != 7">
<app-profile-actions [event]="event" [pubkey]="event.pubkey"></app-profile-actions>
<app-profile-header [pubkey]="event.pubkey"

View File

@ -11,6 +11,7 @@ import { SettingsService } from '../services/settings.service';
import { FeedService } from '../services/feed.service';
import { map, Observable } from 'rxjs';
import { OptionsService } from '../services/options.service';
import { ThreadService } from '../services/thread.service';
@Component({
selector: 'app-note',
@ -59,6 +60,7 @@ export class NoteComponent {
public options: OptionsService,
public feedService: FeedService,
public profiles: ProfileService,
public thread: ThreadService,
private validator: DataValidation,
private utilities: Utilities,
private router: Router
@ -109,27 +111,64 @@ export class NoteComponent {
return this.event.tags.filter((t) => t[0] === 'p').map((t) => t[1]);
}
parentEvent?: NostrEventDocument;
/** Returns the root event, first looks for "root" attribute on the e tag element or picks first in array. */
rootEvent() {
if (!this.event) {
return;
}
// TODO. All of this parsing of arrays is silly and could be greatly improved with some refactoring
// whenever I have time for it.
const eTags = this.event.tags.filter((t) => t[0] === 'e');
for (let i = 0; i < eTags.length; i++) {
const tag = eTags[i];
// If more than 4, we likely have "root" or "reply"
if (tag.length > 3) {
if (tag[3] == 'root') {
return tag[1];
}
}
}
return eTags[0][1];
}
ngOnInit() {
console.log('NG INIT ON NOTE:');
// this.appState.title = 'Blockcore Notes';
this.appState.showBackButton = true;
// Subscribe to the event which will update whenever user requests to view a different event.
this.feedService.event$.subscribe((event) => {
console.log('EVENT CHANGED:', event);
// this.feedService.event$(1).subscribe((event) => {
// console.log('EVENT CHANGED:', event);
if (!event) {
return;
}
// if (!event) {
// return;
// }
this.event = event;
// this.event = event;
// Clear the initial thread:
this.feedService.thread = [];
// // Query for root
// // Query all child
// First download all posts, if any, that is mentioned in the e tags.
this.feedService.downloadThread(this.event.id!);
});
// // Get the root event.
// const rootEventId = this.rootEvent();
// if (rootEventId) {
// // Start downloading the root event.
// this.feedService.downloadEvent([rootEventId]);
// }
// // Clear the initial thread:
// this.feedService.thread = [];
// // First download all posts, if any, that is mentioned in the e tags.
// this.feedService.downloadThread(this.event.id!);
// });
this.activatedRoute.paramMap.subscribe(async (params) => {
const id: any = params.get('id');
@ -139,10 +178,11 @@ export class NoteComponent {
return;
}
console.log('ROUTE ACTIVATE WITH ID:', id);
this.feedService.setActiveEvent(id);
this.thread.changeSelectedEvent(id);
this.id = id;
// console.log('ROUTE ACTIVATE WITH ID:', id);
// this.feedService.setActiveEvent(id);
// this.event = this.feedService.events.find((e) => e.id == this.id);
// if (!this.event) {

View File

@ -31,4 +31,32 @@ export class EventService {
return event;
}
/** Returns the root event, first looks for "root" attribute on the e tag element or picks first in array. */
rootEventId(event: NostrEventDocument) {
if (!event) {
return;
}
// TODO. All of this parsing of arrays is silly and could be greatly improved with some refactoring
// whenever I have time for it.
const eTags = event.tags.filter((t) => t[0] === 'e');
for (let i = 0; i < eTags.length; i++) {
const tag = eTags[i];
// If more than 4, we likely have "root" or "reply"
if (tag.length > 3) {
if (tag[3] == 'root') {
return tag[1];
}
}
}
if (eTags.length == 0) {
return null;
}
return eTags[0][1];
}
}

View File

@ -1,8 +1,8 @@
import { Injectable } from '@angular/core';
import { NostrEvent, NostrProfile, NostrEventDocument, NostrProfileDocument, Circle, Person, NostrSubscription } from './interfaces';
import { NostrEvent, NostrProfile, NostrEventDocument, NostrProfileDocument, Circle, Person, NostrSubscription, NostrRelay } from './interfaces';
import * as sanitizeHtml from 'sanitize-html';
import { SettingsService } from './settings.service';
import { Observable, of, BehaviorSubject, map, combineLatest, single } from 'rxjs';
import { tap, delay, timer, takeUntil, timeout, Observable, of, BehaviorSubject, map, combineLatest, single, Subject, Observer, concat, concatMap, switchMap, catchError, race } from 'rxjs';
import { Relay, relayInit, Sub } from 'nostr-tools';
import { v4 as uuidv4 } from 'uuid';
import { StorageService } from './storage.service';
@ -243,38 +243,238 @@ export class FeedService {
}, 5000);
}
// async downloadEvent(ids: string[]) {
// this.relayStorage.list();
// console.log('DOWNLOAD RECENT FOR:', ids);
// const relay = this.relayService.relays[0];
// const backInTime = moment().subtract(12, 'hours').unix();
// // Start subscribing to our people feeds.
// const sub = relay.sub([{ kinds: [1], since: backInTime, ids: ids }], {}) as NostrSubscription;
// sub.loading = true;
// // Keep all subscriptions around so we can close them when needed.
// this.subs.push(sub);
// sub.on('event', (originalEvent: any) => {
// const event = this.eventService.processEvent(originalEvent);
// if (!event) {
// return;
// }
// this.#persist(event);
// });
// sub.on('eose', () => {
// console.log('Initial load of people feed completed.');
// sub.loading = false;
// });
// public subscribe<T>(key: string): Observable<EventBusMetaData> {
// return this.eventBus.asObservable().pipe(
// filter((event: IEventBusMessage) => this.keyMatch(event.key, key)),
// map((event: IEventBusMessage) => event.metadata)
// );
// }
getEvent(id: string): Observable<NostrEventDocument> {
const subject = new Subject<NostrEventDocument>();
return subject.asObservable();
}
// downloadFromRelayIndex(id: string, index: number, relayCount: number): Observable<NostrEventDocument> {
downloadFromRelay(query: any, relay: NostrRelay): Observable<NostrEventDocument[]> {
//const relay = this.relayService.relays[index];
//console.log('downloadFromRelayIndex:', id, index, relayCount);
const observable = new Observable<NostrEventDocument[]>((observer: Observer<NostrEventDocument[]>) => {
const totalEvents: NostrEventDocument[] = [];
console.log('111111111111111:');
const sub = relay.sub([query], {}) as NostrSubscription;
sub.on('event', (originalEvent: any) => {
// console.log('downloadFromRelayIndex: event', id);
const event = this.eventService.processEvent(originalEvent);
console.log('downloadFromRelayIndex: event', event);
if (!event) {
return;
}
totalEvents.unshift(event);
observer.next(totalEvents);
// sub.unsub();
});
sub.on('eose', () => {
// console.log('downloadFromRelayIndex: eose', id);
observer.complete();
sub.unsub();
});
});
// .pipe(race(observable, this.downloadFromRelayIndex(id, index, relayCount)))
// // .pipe(takeUntil(timer(5)), tap(() => { console.log('TAKE UNTIL TAP!') }))
// .pipe(
// // tap(() => { console.log('CONTINUED HAPPENED, SWITCH MAP AND DOWNLOAD AGAIN!') }),
// // timeout(5000),
// // catchError(error => of("Error while request"))
// // takeUntil(myTimer),
// switchMap((data) => {
// ++index;
// console.log('DATA FROM SWITCH MAP:', data);
// return data ? of(data) : this.downloadFromRelayIndex(id, index, relayCount);
// })
// );
// .pipe(concatMap(event => {
// if (event == null) {
// ++index;
// if (index < relayCount) {
// return this.downloadFromRelayIndex(id, index, relayCount));
// // return ;
// }
// }
// });
return observable;
}
// downloadFromRelayIndex(id: string, index: number, relayCount: number): Observable<NostrEventDocument> {
downloadSingleFromRelay(query: any, relay: NostrRelay): Observable<NostrEventDocument> {
//const relay = this.relayService.relays[index];
//console.log('downloadFromRelayIndex:', id, index, relayCount);
console.log('WITH DELAY:');
const observable = new Observable<NostrEventDocument>((observer: Observer<NostrEventDocument>) => {
console.log('111111111111111:');
const sub = relay.sub([query], {}) as NostrSubscription;
sub.on('event', (originalEvent: any) => {
// console.log('downloadFromRelayIndex: event', id);
const event = this.eventService.processEvent(originalEvent);
console.log('downloadFromRelayIndex: event', event);
if (!event) {
return;
}
observer.next(event);
observer.complete();
// sub.unsub();
});
sub.on('eose', () => {
// console.log('downloadFromRelayIndex: eose', id);
// observer.complete();
sub.unsub();
});
});
// .pipe(race(observable, this.downloadFromRelayIndex(id, index, relayCount)))
// // .pipe(takeUntil(timer(5)), tap(() => { console.log('TAKE UNTIL TAP!') }))
// .pipe(
// // tap(() => { console.log('CONTINUED HAPPENED, SWITCH MAP AND DOWNLOAD AGAIN!') }),
// // timeout(5000),
// // catchError(error => of("Error while request"))
// // takeUntil(myTimer),
// switchMap((data) => {
// ++index;
// console.log('DATA FROM SWITCH MAP:', data);
// return data ? of(data) : this.downloadFromRelayIndex(id, index, relayCount);
// })
// );
// .pipe(concatMap(event => {
// if (event == null) {
// ++index;
// if (index < relayCount) {
// return this.downloadFromRelayIndex(id, index, relayCount));
// // return ;
// }
// }
// });
return observable;
}
downloadThread(id: string) {
// TODO: Change the logic to create a new observable for each X milisecond and make them
// race against each other and don't create more observables if a result is found.
const observables = this.relayService.relays.map((r, index) => this.downloadFromRelay({ ['#e']: [id] }, r));
console.log('OBSERVABLES:', observables);
return race(observables);
}
downloadEvent(id: string) {
// TODO: Change the logic to create a new observable for each X milisecond and make them
// race against each other and don't create more observables if a result is found.
const observables = this.relayService.relays.map((r, index) => this.downloadSingleFromRelay({ kinds: [1], ids: [id] }, r));
console.log('OBSERVABLES:', observables);
return race(observables);
// const observable = new Observable<NostrEventDocument>((observer: Observer<NostrEventDocument>) => {
// const observables = this.relayService.relays.map((r => { this.downloadFromRelayIndex(id, r); }));
// race(observables);
// // for (let i = 0; i < this.relayService.relays.length; i++) {
// // const relay = this.relayService.relays[i];
// // if (relay.status != 1) {
// // continue;
// // }
// // this.downloadFromRelay(id, relay);
// // }
// // this.downloadFromRelayIndex(id, 0, this.relayService.relays.length).subscribe((event) => {
// // console.log('FINIHED DOWNLOAD OBSERSVABLE:', event);
// // observer.next(event);
// // });
// // this.downloadFromRelayIndex(id, 0);
// // const relayIndex = 0;
// // const relay = this.relayService.relays[0];
// // this.downloadFromRelay(id, relay).subscribe((event) => {
// // console.log('COMPLETED FIRST DOWNLAOD FROM RELAY:', event);
// // });
// // for (let i = 0; i < this.relayService.relays.length; i++) {
// // const relay = this.relayService.relays[i];
// // if (relay.status != 1) {
// // continue;
// // }
// // this.downloadFromRelay(id, relay);
// // }
// // console.log('DOWNLOAD EVENT:', id);
// // const relay = this.relayService.relays[0];
// // // Start subscribing to our people feeds.
// // const sub = relay.sub([{ kinds: [1], ids: [id] }], {}) as NostrSubscription;
// // // Keep all subscriptions around so we can close them when needed.
// // // this.subs.push(sub);
// // sub.on('event', (originalEvent: any) => {
// // const event = this.eventService.processEvent(originalEvent);
// // if (!event) {
// // return;
// // }
// // downloadedEvent = event;
// // observer.next(event);
// // // this.#persist(event);
// // });
// // sub.on('eose', () => {
// // console.log('Initial load of people feed completed.');
// // // const subIndex = this.subs.findIndex(sub);
// // if (downloadedEvent) {
// // observer.complete();
// // sub.unsub();
// // } else {
// // // Go to next relay and try there.
// // // First let us unsub the current subscription.
// // sub.unsub();
// // }
// // });
// });
// return observable;
// get rootEvents$(): Observable<NostrEventDocument[]> {
// return this.events$.pipe(
// map((data) => {
// const filtered = data.filter((events) => !events.tags.find((t) => t[0] === 'e'));
// return filtered;
// })
// );
// }
// this.relayStorage.list();
}
async downloadRecent(pubkeys: string[]) {
console.log('DOWNLOAD RECENT FOR:', pubkeys);
const relay = this.relayService.relays[0];
@ -311,8 +511,7 @@ export class FeedService {
// threadQueue: string[];
downloadThread(id: string) {
downloadThread2(id: string) {
// First get existing data.
// this.#filter((e) => { e.tags. });

View File

@ -20,13 +20,14 @@ import { RelayStorageService } from './relay.storage.service';
export class RelayService {
/** Default relays that the app has for users without extension. This follows the document structure as extension data. */
defaultRelays: any = {
'wss://relay.nostr.info': { read: true, write: true },
'wss://nostr-pub.wellorder.net': { read: true, write: true },
'wss://nostr.nordlysln.net': { read: true, write: true },
// 'wss://nostr-verified.wellorder.net': { read: false, write: true },
// 'wss://nostr.bitcoiner.social': { read: true, write: true },
// 'wss://nostr.drss.io': { read: true, write: true },
'wss://relay.damus.io': { read: true, write: false },
'wss://relay.nostr.info': { read: true, write: true },
// 'wss://relay.nostr.info': { read: true, write: true },
// 'wss://relay.minds.com/nostr/v1/ws': { read: false, write: true },
'wss://relay.nostr.ch': { read: true, write: true },
};

View File

@ -0,0 +1,161 @@
import { Injectable } from '@angular/core';
import { NostrEvent, NostrProfile, NostrEventDocument, NostrProfileDocument, Circle, Person, NostrSubscription } from './interfaces';
import * as sanitizeHtml from 'sanitize-html';
import { SettingsService } from './settings.service';
import { Observable, of, BehaviorSubject, map, combineLatest, single, ReplaySubject, mergeMap } from 'rxjs';
import { Relay, relayInit, Sub } from 'nostr-tools';
import { v4 as uuidv4 } from 'uuid';
import { StorageService } from './storage.service';
import { ProfileService } from './profile.service';
import { CirclesService } from './circles.service';
import * as moment from 'moment';
import { EventService } from './event.service';
import { DataValidation } from './data-validation.service';
import { OptionsService } from './options.service';
import { RelayService } from './relay.service';
import { RelayStorageService } from './relay.storage.service';
import { FeedService } from './feed.service';
@Injectable({
providedIn: 'root',
})
export class ThreadService {
#event: NostrEventDocument | null = null;
#eventChanged: BehaviorSubject<NostrEventDocument | null> = new BehaviorSubject<NostrEventDocument | null>(this.#event);
event$ = this.#eventChanged.asObservable();
#root: NostrEventDocument | null = null;
#rootChanged: BehaviorSubject<NostrEventDocument | null> = new BehaviorSubject<NostrEventDocument | null>(this.#root);
root$ = this.#rootChanged.asObservable();
#events: NostrEventDocument[] = [];
#eventsChanged: BehaviorSubject<NostrEventDocument[]> = new BehaviorSubject<NostrEventDocument[]>(this.#events);
// #beforeChanged: BehaviorSubject<NostrEventDocument[]> = new BehaviorSubject<NostrEventDocument[]>(this.#events);
// before$ = this.#beforeChanged.asObservable();
// #afterChanged: BehaviorSubject<NostrEventDocument[]> = new BehaviorSubject<NostrEventDocument[]>(this.#events);
// after$ = this.#afterChanged.asObservable();
get before$(): Observable<NostrEventDocument[]> {
return this.#eventsChanged
.asObservable()
.pipe(
map((data) => {
data.sort((a, b) => {
return a.created_at > b.created_at ? -1 : 1;
});
return data;
})
)
.pipe(map((data) => data.filter((events) => !this.profileService.blockedPublickKeys().includes(events.pubkey))));
}
get after$(): Observable<NostrEventDocument[]> {
return this.#eventsChanged
.asObservable()
.pipe(
map((data) => {
data.sort((a, b) => {
return a.created_at > b.created_at ? -1 : 1;
});
return data;
})
)
.pipe(map((data) => data.filter((events) => !this.profileService.blockedPublickKeys().includes(events.pubkey))));
}
get events$(): Observable<NostrEventDocument[]> {
return this.#eventsChanged
.asObservable()
.pipe(
map((data) => {
data.sort((a, b) => {
return a.created_at > b.created_at ? -1 : 1;
});
return data;
})
)
.pipe(map((data) => data.filter((events) => !this.profileService.blockedPublickKeys().includes(events.pubkey))));
}
get rootEvents$(): Observable<NostrEventDocument[]> {
return this.events$.pipe(
map((data) => {
const filtered = data.filter((events) => !events.tags.find((t) => t[0] === 'e'));
return filtered;
})
);
}
#selectedEventSource = new ReplaySubject<string>();
selectedEventChanges$ = this.#selectedEventSource.asObservable();
#eventSource = new ReplaySubject<NostrEventDocument>();
eventChanges$ = this.#eventSource.asObservable();
// selectedEvent$ = combineLatest(this.selectedEventChanges$, this.eventChanges$).pipe(mergeMap());
constructor(private storage: StorageService, private eventService: EventService, private profileService: ProfileService, private feedService: FeedService) {
// Whenever the event has changed, we can go grab the parent and the thread itself
this.#eventChanged.subscribe((event) => {
if (event == null) {
console.log('EVENT WAS NULL!');
return;
}
console.log('EVENT CHANGED!!!', event);
// Get the root event.
const rootEventId = this.eventService.rootEventId(event);
console.log('ROOT EVENT ID:', rootEventId);
if (rootEventId) {
this.feedService.downloadEvent(rootEventId).subscribe((event) => {
console.log(event);
this.#root = event;
this.#rootChanged.next(this.#root);
console.log('GOT ROOT EVENT', this.#root);
});
// Get all events in the thread.
this.feedService.downloadThread(rootEventId).subscribe((events) => {
console.log(events);
this.#events = events;
this.#eventsChanged.next(this.#events);
console.log('GOT ROOT EVENT', this.#root);
});
} else {
this.#root = null;
// Reset the root if the current event does not have one.
this.#rootChanged.next(this.#root);
}
});
}
async changeSelectedEvent(eventId: string) {
// Get the event itself.
const event = await this.storage.get<NostrEventDocument>(eventId);
if (event) {
this.#event = event;
this.#eventChanged.next(this.#event);
} else {
// Go grab it from relays.
this.feedService.downloadEvent(eventId).subscribe((event) => {
console.log(event);
this.#eventChanged.next(event);
});
}
}
/** Fetches the whole thread of data to be rendered in the UI. */
async fetchThread(id: string) {
// Second get the original post to render ontop of the page and all of the other events in e tags.
// Third get the replies to the id supplied.
}
}

View File

@ -205,6 +205,11 @@ mat-sidenav-content mat-toolbar {
margin: 0;
}
.parent-events {
margin-left: 3em;
background-color: #a30770 !important;
}
.events {
margin-top: 1em;
padding: 1em;