mirror of
https://github.com/block-core/blockcore-notes.git
synced 2024-09-29 06:20:42 +00:00
Add contact list import
- WIP: Get the lists, doesn't do import yet. - Lists number of following on the Circles
This commit is contained in:
parent
a8cf0af8c3
commit
24cec0c35e
@ -66,6 +66,7 @@ import { CircleStylePipe } from './shared/circle-style';
|
||||
import { ReplyListComponent } from './shared/reply-list/reply-list.component';
|
||||
import { ContentComponent } from './shared/content/content.component';
|
||||
import { ScrollDirective } from './shared/scroll.directive';
|
||||
import { ImportFollowDialog } from './circles/import-follow-dialog/import-follow-dialog';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -96,7 +97,8 @@ import { ScrollDirective } from './shared/scroll.directive';
|
||||
NoteComponent,
|
||||
ReplyListComponent,
|
||||
ContentComponent,
|
||||
ScrollDirective
|
||||
ScrollDirective,
|
||||
ImportFollowDialog,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
@ -15,7 +15,6 @@
|
||||
}
|
||||
|
||||
.circle-button {
|
||||
|
||||
}
|
||||
|
||||
.circle-button-icon {
|
||||
@ -41,16 +40,21 @@
|
||||
order: 0;
|
||||
flex: 0 1 auto;
|
||||
align-self: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.circle-item:nth-child(2) {
|
||||
order: 0;
|
||||
flex: 1 1 auto;
|
||||
align-self: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.circle-item:nth-child(3) {
|
||||
order: 0;
|
||||
flex: 0 1 auto;
|
||||
align-self: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.circle-actions button {
|
||||
margin-right: 0.4em;
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
|
@ -5,7 +5,10 @@
|
||||
<div class="circle-container" *ngFor="let circle of circles">
|
||||
<div class="circle-item"><mat-icon matListItemIcon [style.color]="circle.color">trip_origin</mat-icon></div>
|
||||
<div class="circle-item">
|
||||
{{ circle.name }}<br /><span class="dimmed"><span *ngIf="circle.public">Public</span><span *ngIf="!circle.public">Private</span> - {{ circle.style | circlestyle }} - Created: {{ circle.created | ago }}</span>
|
||||
{{ circle.name }}<br />
|
||||
<span class="dimmed"><span *ngIf="circle.public">Count: {{ countMembers(circle) }}</span></span><br />
|
||||
<span class="dimmed"><span *ngIf="circle.public">Public</span>
|
||||
<span *ngIf="!circle.public">Private</span> - {{ circle.style | circlestyle }} - Created: {{ circle.created | ago }}</span>
|
||||
</div>
|
||||
<div class="circle-item">
|
||||
<button (click)="deleteCircle(circle.id)" *ngIf="circle.id" class="circle-button" mat-icon-button>
|
||||
@ -14,6 +17,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="dimmed">Circles is how you organize people you follow. Different circles can have different rules applied and circles is an important way to make the experience more enjoyable.</p>
|
||||
<p class="circle-actions">
|
||||
<button mat-stroked-button (click)="importFollowList()">Import Follow List</button>
|
||||
<button mat-stroked-button (click)="publishFollowList()">Publish Follow List</button>
|
||||
</p>
|
||||
|
||||
<p class="dimmed">Circles is how you organize people you follow. Different circles can have different rules applied and circles is an important way to make the experience more enjoyable.</p>
|
||||
</div>
|
||||
|
@ -10,6 +10,10 @@ import { CirclesService } from '../services/circles.service';
|
||||
import { CircleDialog } from '../shared/create-circle-dialog/create-circle-dialog';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { ImportFollowDialog, ImportFollowDialogData } from './import-follow-dialog/import-follow-dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { AuthenticationService } from '../services/authentication.service';
|
||||
import { FeedService } from '../services/feed.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-circles',
|
||||
@ -26,9 +30,12 @@ export class CirclesComponent {
|
||||
private storage: StorageService,
|
||||
private profile: ProfileService,
|
||||
public dialog: MatDialog,
|
||||
private feedService: FeedService,
|
||||
private validator: DataValidation,
|
||||
private utilities: Utilities,
|
||||
private router: Router
|
||||
private authService: AuthenticationService,
|
||||
private router: Router,
|
||||
private snackBar: MatSnackBar
|
||||
) {}
|
||||
|
||||
// public trackByFn(index: number, item: NostrProfileDocument) {
|
||||
@ -92,6 +99,53 @@ export class CirclesComponent {
|
||||
await this.load();
|
||||
}
|
||||
|
||||
countMembers(circle: Circle) {
|
||||
if (circle.id == null || circle.id == '') {
|
||||
return this.following.filter((f) => f.circle == null).length;
|
||||
} else {
|
||||
return this.following.filter((f) => f.circle == circle.id).length;
|
||||
}
|
||||
}
|
||||
|
||||
async importFollowList() {
|
||||
const dialogRef = this.dialog.open(ImportFollowDialog, {
|
||||
data: { pubkey: this.appState.getPublicKey() },
|
||||
maxWidth: '100vw',
|
||||
panelClass: 'full-width-dialog',
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(async (result: ImportFollowDialogData) => {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.snackBar.open('Importing followers process has started', 'Hide', {
|
||||
horizontalPosition: 'center',
|
||||
verticalPosition: 'bottom',
|
||||
});
|
||||
|
||||
let pubkey = result.pubkey;
|
||||
|
||||
console.log('GET FOLLOWING LIST FOR:', pubkey);
|
||||
|
||||
// TODO: Add ability to slowly query one after one relay, we don't want to receive multiple
|
||||
// follow lists and having to process everything multiple times. Just query one by one until
|
||||
// we find the list. Until then, we simply grab the first relay only.
|
||||
this.feedService.downloadContacts(pubkey).subscribe((contacts) => {
|
||||
console.log('DOWNLOAD COMPLETE!', contacts);
|
||||
});
|
||||
|
||||
// if (pubkey.startsWith('npub')) {
|
||||
// pubkey = this.utilities.arrayToHex(this.utilities.convertFromBech32(pubkey));
|
||||
// }
|
||||
|
||||
// await this.profileService.follow(pubkey);
|
||||
// await this.feedService.downloadRecent([pubkey]);
|
||||
});
|
||||
}
|
||||
|
||||
publishFollowList() {}
|
||||
|
||||
createCircle(): void {
|
||||
const dialogRef = this.dialog.open(CircleDialog, {
|
||||
data: { name: '', style: '1', public: true },
|
||||
@ -113,7 +167,11 @@ export class CirclesComponent {
|
||||
});
|
||||
}
|
||||
|
||||
following: NostrProfileDocument[] = [];
|
||||
|
||||
async ngOnInit() {
|
||||
this.following = await this.profile.followList();
|
||||
|
||||
this.appState.title = 'Circles';
|
||||
this.appState.showBackButton = true;
|
||||
this.appState.actions = [
|
||||
|
@ -0,0 +1,14 @@
|
||||
<h1 mat-dialog-title>Import complete following list</h1>
|
||||
<div mat-dialog-content class="mat-dialog-content">
|
||||
<p>The existing value is your own public key, use this to import your own public following list.</p>
|
||||
<mat-form-field appearance="fill" class="input-full-width">
|
||||
<mat-icon class="circle" matPrefix>person_add</mat-icon>
|
||||
<mat-label>Public Key</mat-label>
|
||||
<input matInput type="text" autocomplete="off" [(ngModel)]="data.pubkey" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div mat-dialog-actions class="mat-dialog-actions" align="end">
|
||||
<button mat-stroked-button (click)="onNoClick()">Cancel</button>
|
||||
<button mat-flat-button [mat-dialog-close]="data" color="primary" cdkFocusInitial>Import</button>
|
||||
</div>
|
@ -0,0 +1,11 @@
|
||||
.input-full-width {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.mat-dialog-content {
|
||||
padding: 20px 24px 0px 24px !important;
|
||||
}
|
||||
|
||||
.mat-dialog-actions {
|
||||
padding: 10px 24px 24px 24px;
|
||||
}
|
22
src/app/circles/import-follow-dialog/import-follow-dialog.ts
Normal file
22
src/app/circles/import-follow-dialog/import-follow-dialog.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MatDialog, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
|
||||
export interface ImportFollowDialogData {
|
||||
pubkey: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'import-follow-dialog',
|
||||
templateUrl: 'import-follow-dialog.html',
|
||||
styleUrls: ['import-follow-dialog.scss'],
|
||||
})
|
||||
export class ImportFollowDialog {
|
||||
constructor(public dialogRef: MatDialogRef<ImportFollowDialogData>, @Inject(MAT_DIALOG_DATA) public data: ImportFollowDialogData) {
|
||||
|
||||
}
|
||||
|
||||
onNoClick(): void {
|
||||
this.data.pubkey = '';
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
@ -147,7 +147,7 @@ export class FeedPublicComponent {
|
||||
}
|
||||
|
||||
event = this.validator.sanitizeEvent(event);
|
||||
event = this.validator.filterEvent(event);
|
||||
// event = this.validator.filterEvent(event);
|
||||
|
||||
if (!event) {
|
||||
return null;
|
||||
|
@ -1,13 +1,14 @@
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Subject, map, shareReplay, Observable } from 'rxjs';
|
||||
import { AuthenticationService } from './authentication.service';
|
||||
import { Action } from './interfaces';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ApplicationState {
|
||||
constructor(private breakpointObserver: BreakpointObserver) {
|
||||
constructor(private breakpointObserver: BreakpointObserver, private authService: AuthenticationService) {
|
||||
this.isSmallScreen$ = this.breakpointObserver.observe('(max-width: 599px)').pipe(
|
||||
map((result) => result.matches),
|
||||
shareReplay()
|
||||
@ -19,6 +20,10 @@ export class ApplicationState {
|
||||
);
|
||||
}
|
||||
|
||||
getPublicKey() {
|
||||
return this.authService.authInfo$.getValue().publicKeyHex;
|
||||
}
|
||||
|
||||
title = 'Blockcore Notes';
|
||||
|
||||
goBack = false;
|
||||
|
@ -14,6 +14,8 @@ export class DataValidation {
|
||||
profileLimit = 2048;
|
||||
profileTagsLimit = 10;
|
||||
|
||||
contactsContentLimit = 2048;
|
||||
|
||||
constructor(private options: OptionsService) {}
|
||||
|
||||
sanitizeEvent(event: NostrEvent) {
|
||||
@ -107,6 +109,41 @@ export class DataValidation {
|
||||
return event;
|
||||
}
|
||||
|
||||
/** Returns true if valid, false if not valid. Does not throw error for optimization purposes. */
|
||||
validateContacts(event: NostrEvent) {
|
||||
if (event.pubkey.length < 60 || event.pubkey.length > 70) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!event.sig || !event.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (event.sig.length < 100 || event.pubkey.length > 150) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (event.id.length !== 64) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof event.kind !== 'number' || typeof event.created_at !== 'number') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (event.kind !== 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Reduce the content length to reduce system resource usage and improve UI experience.
|
||||
if (event.content.length > this.contactsContentLimit) {
|
||||
event.content = event.content.substring(0, this.contactsContentLimit);
|
||||
event.contentCut = true;
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
escapeNewLineChars(valueToEscape: string) {
|
||||
if (valueToEscape != null && valueToEscape != '') {
|
||||
return valueToEscape.replace(/\n/g, ' ');
|
||||
|
@ -19,7 +19,7 @@ export class EventService {
|
||||
}
|
||||
|
||||
event = this.validator.sanitizeEvent(event);
|
||||
event = this.validator.filterEvent(event);
|
||||
// event = this.validator.filterEvent(event);
|
||||
|
||||
if (!event) {
|
||||
return null;
|
||||
@ -32,6 +32,29 @@ export class EventService {
|
||||
return event;
|
||||
}
|
||||
|
||||
processContacts(originalEvent: NostrEvent): NostrEvent | null {
|
||||
// Validate the event:
|
||||
let event = this.validator.validateContacts(originalEvent);
|
||||
|
||||
if (!event) {
|
||||
debugger;
|
||||
console.log('INVALID CONTACT EVENT!');
|
||||
return null;
|
||||
}
|
||||
|
||||
// event = this.validator.filterEvent(event);
|
||||
|
||||
// if (!event) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// TODO: Store the raw event.
|
||||
// const nostrEvent = event as NostrEventDocument;
|
||||
// nostrEvent.raw = originalEvent;
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
/** Returns the root event, first looks for "root" attribute on the e tag element or picks first in array. */
|
||||
eTags(event: NostrEventDocument | null) {
|
||||
if (!event) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { NostrEvent, NostrProfile, NostrEventDocument, NostrProfileDocument, Circle, Person, NostrSubscription, NostrRelay } from './interfaces';
|
||||
import { NostrEvent, NostrProfile, NostrEventDocument, NostrProfileDocument, Circle, Person, NostrSubscription, NostrRelay, Contact } from './interfaces';
|
||||
import * as sanitizeHtml from 'sanitize-html';
|
||||
import { SettingsService } from './settings.service';
|
||||
import { tap, delay, timer, takeUntil, timeout, Observable, of, BehaviorSubject, map, combineLatest, single, Subject, Observer, concat, concatMap, switchMap, catchError, race } from 'rxjs';
|
||||
@ -14,6 +14,7 @@ import { DataValidation } from './data-validation.service';
|
||||
import { OptionsService } from './options.service';
|
||||
import { RelayService } from './relay.service';
|
||||
import { RelayStorageService } from './relay.storage.service';
|
||||
import { AuthenticationService } from './authentication.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@ -132,6 +133,7 @@ export class FeedService {
|
||||
private relayService: RelayService,
|
||||
private eventService: EventService,
|
||||
private validator: DataValidation,
|
||||
private authService: AuthenticationService,
|
||||
private storage: StorageService,
|
||||
private profileService: ProfileService,
|
||||
private circlesService: CirclesService
|
||||
@ -715,6 +717,109 @@ export class FeedService {
|
||||
}
|
||||
}
|
||||
|
||||
downloadContacts(pubkey: string) {
|
||||
//const relay = this.relayService.relays[index];
|
||||
//console.log('downloadFromRelayIndex:', id, index, relayCount);
|
||||
const relay = this.relayService.relays[0];
|
||||
let finished = false;
|
||||
|
||||
const observable = new Observable<NostrEventDocument>((observer: Observer<NostrEventDocument>) => {
|
||||
const sub = relay.sub([{ kinds: [3], authors: [pubkey] }], {}) as NostrSubscription;
|
||||
|
||||
sub.on('event', (originalEvent: any) => {
|
||||
debugger;
|
||||
// console.log('downloadFromRelayIndex: event', id);
|
||||
const event = this.eventService.processContacts(originalEvent);
|
||||
// console.log('downloadFromRelayIndex: event', event);
|
||||
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
|
||||
finished = true;
|
||||
|
||||
observer.next(event);
|
||||
observer.complete();
|
||||
// sub.unsub();
|
||||
});
|
||||
|
||||
sub.on('eose', () => {
|
||||
debugger;
|
||||
// If we did not find the data, we must finish.
|
||||
if (!finished) {
|
||||
observer.complete();
|
||||
}
|
||||
// console.log('downloadFromRelayIndex: eose', id);
|
||||
// observer.complete();
|
||||
sub.unsub();
|
||||
});
|
||||
});
|
||||
|
||||
return observable;
|
||||
}
|
||||
|
||||
async publishContacts(contacts: Contact[]) {
|
||||
const mappedContacts = contacts.map((c) => {
|
||||
return ['p', c.pubkey];
|
||||
});
|
||||
|
||||
let originalEvent: Event = {
|
||||
kind: 3,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
content: '',
|
||||
pubkey: this.authService.authInfo$.getValue().publicKeyHex!,
|
||||
tags: mappedContacts,
|
||||
};
|
||||
|
||||
originalEvent.id = getEventHash(originalEvent);
|
||||
|
||||
const gt = globalThis as any;
|
||||
|
||||
// Use nostr directly on global, similar to how most Nostr app will interact with the provider.
|
||||
const signedEvent = await gt.nostr.signEvent(originalEvent);
|
||||
originalEvent = signedEvent;
|
||||
|
||||
// We force validation upon user so we make sure they don't create content that we won't be able to parse back later.
|
||||
// We must do this before we run nostr-tools validate and signature validation.
|
||||
const event = this.eventService.processEvent(originalEvent as NostrEventDocument);
|
||||
|
||||
let ok = validateEvent(originalEvent);
|
||||
|
||||
if (!ok) {
|
||||
throw new Error('The event is not valid. Cannot publish.');
|
||||
}
|
||||
|
||||
let veryOk = await verifySignature(originalEvent as any); // Required .id and .sig, which we know has been added at this stage.
|
||||
|
||||
if (!veryOk) {
|
||||
throw new Error('The event signature not valid. Maybe you choose a different account than the one specified?');
|
||||
}
|
||||
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('PUBLISH EVENT:', originalEvent);
|
||||
|
||||
// First we persist our own event like would normally happen if we receive this event.
|
||||
// await this.#persist(event);
|
||||
|
||||
// for (let i = 0; i < this.relayService.relays.length; i++) {
|
||||
// const relay = this.relayService.relays[i];
|
||||
|
||||
// let pub = relay.publish(event);
|
||||
// pub.on('ok', () => {
|
||||
// console.log(`${relay.url} has accepted our event`);
|
||||
// });
|
||||
// pub.on('seen', () => {
|
||||
// console.log(`we saw the event on ${relay.url}`);
|
||||
// });
|
||||
// pub.on('failed', (reason: any) => {
|
||||
// console.log(`failed to publish to ${relay.url}: ${reason}`);
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
// Whenever the profile service needs to get a profile from the network, this event is triggered.
|
||||
// this.profileService.profileRequested$.subscribe(async (pubkey) => {
|
||||
|
@ -10,6 +10,12 @@ export interface Circle {
|
||||
public: boolean;
|
||||
}
|
||||
|
||||
export interface Contact {
|
||||
pubkey: string;
|
||||
relay?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface Action {
|
||||
tooltip: string;
|
||||
icon: string;
|
||||
|
Loading…
Reference in New Issue
Block a user