From e7f5e23ae4fb65521e499705d09de0d74ae7d439 Mon Sep 17 00:00:00 2001 From: d-krause Date: Mon, 6 Mar 2023 13:41:12 -0500 Subject: [PATCH 01/39] fixing of ExpressionChangedAfterItHasBeenCheckedError in profile. commenting out debugger line --- src/app/profile/profile.ts | 6 ++++-- src/app/types/storage.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/app/profile/profile.ts b/src/app/profile/profile.ts index 5a33154e..f2c146c3 100644 --- a/src/app/profile/profile.ts +++ b/src/app/profile/profile.ts @@ -46,10 +46,12 @@ export class ProfileComponent { private profileService: ProfileService, private dataService: DataService, private activatedRoute: ActivatedRoute - ) {} + ) { - async ngOnInit() { this.appState.updateTitle('Edit Profile'); + } + + async ngOnInit() { this.originalProfile = { name: '', diff --git a/src/app/types/storage.ts b/src/app/types/storage.ts index 3c000651..b81a2080 100644 --- a/src/app/types/storage.ts +++ b/src/app/types/storage.ts @@ -70,7 +70,7 @@ export class Storage { async open() { this.db = await openDB(this.name, 2, { upgrade(db, oldVersion, newVersion, transaction, event) { - debugger; + // debugger; switch (oldVersion) { case 0: From 42babaed474b40b0f4ada6ec292b17a212da907d Mon Sep 17 00:00:00 2001 From: d-krause Date: Mon, 6 Mar 2023 13:46:37 -0500 Subject: [PATCH 02/39] adding Observable to NostrRelaySubscription type - makes it easier to listen for events from the component or service where they are subscribed. --- src/app/services/interfaces.ts | 2 ++ src/app/services/relay.ts | 20 ++++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/app/services/interfaces.ts b/src/app/services/interfaces.ts index 33ebf316..e5939c9c 100644 --- a/src/app/services/interfaces.ts +++ b/src/app/services/interfaces.ts @@ -1,4 +1,5 @@ import { Event, Filter, Relay, Sub } from 'nostr-tools'; +import { SubjectLike } from 'rxjs'; export interface Circle { id?: number; @@ -82,6 +83,7 @@ export interface NostrRelaySubscription { events: Event[]; // events: Map; // events$: any; + observable?: SubjectLike | undefined; type: string | 'Profile' | 'Event' | 'Contacts' | 'Article' | 'BadgeDefinition'; } diff --git a/src/app/services/relay.ts b/src/app/services/relay.ts index b3002a7c..65a64b07 100644 --- a/src/app/services/relay.ts +++ b/src/app/services/relay.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { LoadMoreOptions, NostrRelay, NostrRelayDocument, NostrRelaySubscription, QueryJob } from './interfaces'; -import { Observable, BehaviorSubject } from 'rxjs'; +import { Observable, BehaviorSubject, SubjectLike } from 'rxjs'; import { Event, Filter, Kind } from 'nostr-tools'; import { EventService } from './event'; import { OptionsService } from './options'; @@ -405,6 +405,9 @@ export class RelayService { if (index === -1) { sub.events.push(event); + if(sub.observable) { + sub.observable.next(event); + } } } else if (sub.type == 'Profile') { const index = sub.events.findIndex((e) => e.pubkey == event.pubkey); @@ -432,10 +435,16 @@ export class RelayService { if (index > -1) { if (event.created_at > sub.events[index].created_at) { sub.events[index] = event; + if(sub.observable) { + sub.observable.next(event); + } await this.badgeService.putDefinition(event); } } else { sub.events.push(event); + if(sub.observable) { + sub.observable.next(event); + } await this.badgeService.putDefinition(event); } @@ -801,12 +810,12 @@ export class RelayService { return id; } - subscribe(filters: Filter[], id?: string, type: string = 'Event') { + subscribe(filters: Filter[], id?: string, type: string = 'Event', observable: SubjectLike | undefined = undefined) { if (!id) { id = uuidv4(); } - const sub = { id: id, filters: filters, events: [], type: type }; + const sub = { id: id, filters: filters, events: [], type: type, observable }; // this.action('subscribe', { filters, id }); this.subs.set(id, sub); @@ -868,7 +877,10 @@ export class RelayService { const worker = this.workers[index]; worker.unsubscribe(id); } - + const sub = this.subs.get(id); + if(sub && sub.observable) { + sub.observable.complete(); + } this.subs.delete(id); } From d0d8c0d48960c1b626e19a7ac6b946df0932e10f Mon Sep 17 00:00:00 2001 From: d-krause Date: Mon, 6 Mar 2023 13:47:38 -0500 Subject: [PATCH 03/39] NIP76 initial commit --- package.json | 2 + src/app/app-routing.module.ts | 17 ++ src/app/app.module.ts | 2 + src/app/profile/profile.html | 3 +- src/app/relays/relays.html | 4 +- src/app/relays/relays.ts | 7 + src/app/services/nip76.service.spec.ts | 16 ++ src/app/services/nip76.service.ts | 146 ++++++++++++ .../private-threads.component.html | 91 +++++++ .../private-threads.component.scss | 33 +++ .../private-threads.component.spec.ts | 23 ++ .../private-threads.component.ts | 223 ++++++++++++++++++ tsconfig.json | 13 +- 13 files changed, 577 insertions(+), 3 deletions(-) create mode 100644 src/app/services/nip76.service.spec.ts create mode 100644 src/app/services/nip76.service.ts create mode 100644 src/app/shared/private-threads/private-threads.component.html create mode 100644 src/app/shared/private-threads/private-threads.component.scss create mode 100644 src/app/shared/private-threads/private-threads.component.spec.ts create mode 100644 src/app/shared/private-threads/private-threads.component.ts diff --git a/package.json b/package.json index 710c961d..d4ef6c82 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,8 @@ "@twogate/ngx-photo-gallery": "^1.4.0", "@types/sharedworker": "^0.0.91", "angularx-qrcode": "^15.0.1", + "animiq-lib": "file:../../animiq-lib/dist/animiq-lib", + "animiq-nip76-tools": "file:../../animiq-nip76-tools/dist", "dexie": "^3.2.3", "html5-qrcode": "^2.3.7", "idb": "^7.1.1", diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 71442b2a..6237ea02 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -33,6 +33,7 @@ import { LoginComponent } from './connect/login/login'; import { CreateProfileComponent } from './connect/create/create'; import { EditorBadgesComponent } from './editor-badges/editor'; import { BadgeComponent } from './badge/badge'; +import { PrivateThreadsComponent } from './shared/private-threads/private-threads.component'; const routes: Routes = [ { @@ -139,6 +140,22 @@ const routes: Routes = [ data: LoadingResolverService, }, }, + { + path: 'profile/private-threads', + component: PrivateThreadsComponent, + canActivate: [AuthGuard], + resolve: { + data: LoadingResolverService, + }, + }, + { + path: 'profile/private-threads/:threadPubKey', + component: PrivateThreadsComponent, + canActivate: [AuthGuard], + resolve: { + data: LoadingResolverService, + }, + }, { path: 'badges/:id', component: BadgesComponent, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index e5eaa090..67753903 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -151,6 +151,7 @@ import { EditorBadgesComponent } from './editor-badges/editor'; import { BadgeCardComponent } from './shared/badge-card/badge-card'; import { TagsComponent } from './shared/tags/tags'; import { BadgeComponent } from './badge/badge'; +import { PrivateThreadsComponent } from './shared/private-threads/private-threads.component'; import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; @NgModule({ @@ -243,6 +244,7 @@ import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; BadgeCardComponent, TagsComponent, BadgeComponent, + PrivateThreadsComponent, ], imports: [ AboutModule, diff --git a/src/app/profile/profile.html b/src/app/profile/profile.html index f97afaac..d5334214 100644 --- a/src/app/profile/profile.html +++ b/src/app/profile/profile.html @@ -2,8 +2,9 @@
+ Manage Private Threads - +

badge diff --git a/src/app/relays/relays.html b/src/app/relays/relays.html index e30c28e2..fb4d9945 100644 --- a/src/app/relays/relays.html +++ b/src/app/relays/relays.html @@ -10,7 +10,9 @@

- + + +


diff --git a/src/app/relays/relays.ts b/src/app/relays/relays.ts index 64bd9065..d0c90d89 100644 --- a/src/app/relays/relays.ts +++ b/src/app/relays/relays.ts @@ -92,6 +92,13 @@ export class RelaysManagementComponent { this.wipedNotes = true; } + async getLocalDevRelays() { + const relays = { + 'wss://nostr-dev.animiq.com' : { read: true, write: true } + } + await this.relayService.appendRelays(relays); + } + async getDefaultRelays() { // Append the default relays. await this.relayService.appendRelays(this.nostr.defaultRelays); diff --git a/src/app/services/nip76.service.spec.ts b/src/app/services/nip76.service.spec.ts new file mode 100644 index 00000000..9a524647 --- /dev/null +++ b/src/app/services/nip76.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { Nip76Service } from './nip76.service'; + +describe('Nip76Service', () => { + let service: Nip76Service; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(Nip76Service); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/nip76.service.ts b/src/app/services/nip76.service.ts new file mode 100644 index 00000000..d863da36 --- /dev/null +++ b/src/app/services/nip76.service.ts @@ -0,0 +1,146 @@ +import { Injectable } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { HDKey, PrivateThread, Versions, Nip76Wallet, HDKissAddress, HDKissDocumentType, nip19Extension, IThreadPayload, PostDocument } from 'animiq-nip76-tools'; +import { Event, getEventHash, signEvent, validateEvent } from 'nostr-tools'; +import { BehaviorSubject } from 'rxjs'; +import { PasswordDialog, PasswordDialogData } from '../shared/password-dialog/password-dialog'; +import { DataService } from './data'; +import { SecurityService } from './security'; +`` +const sessionKeyAddress = 'blockcore:notes:nostr:nip76:sessionKey'; +const nostrPrivKeyAddress = 'blockcore:notes:nostr:prvkey'; + +@Injectable({ + providedIn: 'root' +}) +export class Nip76Service { + wallet: Nip76Wallet; + privateThreads = new BehaviorSubject([]); + constructor( + private dialog: MatDialog, + private snackBar: MatSnackBar, + private security: SecurityService, + private dataService: DataService + ) { + this.wallet = new Nip76Wallet(); + if (this.wallet.isInSession) { + const sessionKey = sessionStorage.getItem(sessionKeyAddress); + if (sessionKey) { + if (this.wallet.readKey(sessionKey, 'session', '')) { + this.initThreadProfiles(); + } else { + sessionStorage.removeItem(sessionKeyAddress); + this.wallet.clearSession(); + } + } else { + this.loadKeyFromPassword(); + } + } else if (this.wallet.requiresLogin) { + this.loadKeyFromPassword(); + } + } + private save(password: string) { + this.wallet.saveKey(password, 'backup', false); + const sessionKey = this.wallet.generateSessionKey(); + sessionStorage.setItem(sessionKeyAddress, sessionKey); + this.wallet.saveKey(sessionKey, 'session', false); + } + async saveKeyWithPassword(): Promise { + return new Promise((resolve, reject) => { + const dialogRef = this.dialog.open(PasswordDialog, { + data: { action: 'Save Private Thread Keys', password: '' }, + maxWidth: '100vw', + panelClass: 'full-width-dialog', + }); + dialogRef.afterClosed().subscribe(async (result: PasswordDialogData) => { + if (result) { + const prvkeyEncrypted = localStorage.getItem(nostrPrivKeyAddress); + const prvkey = await this.security.decryptData(prvkeyEncrypted!, result.password); + if (prvkey) { + this.save(prvkey); + resolve(true); + } else { + this.snackBar.open(`Unable to decrypt data. Probably wrong password. Try again.`, 'Hide', { + duration: 3000, + horizontalPosition: 'center', + verticalPosition: 'bottom', + }); + reject(); + } + } else { + reject(); + } + }); + }); + } + loadKeyFromPassword() { + const dialogRef = this.dialog.open(PasswordDialog, { + data: { action: 'Load Private Thread Keys', password: '' }, + maxWidth: '100vw', + panelClass: 'full-width-dialog', + }); + dialogRef.afterClosed().subscribe(async (result: PasswordDialogData) => { + if (result) { + const prvkeyEncrypted = localStorage.getItem(nostrPrivKeyAddress); + const prvkey = await this.security.decryptData(prvkeyEncrypted!, result.password); + if (prvkey) { + if (this.wallet.readKey(prvkey, 'backup', '')) { + this.save(prvkey); + this.initThreadProfiles(); + } else { + this.snackBar.open(`Unable to decrypt nip76 data. Probably wrong password. Try again.`, 'Hide', { + duration: 3000, + horizontalPosition: 'center', + verticalPosition: 'bottom', + }); + } + } else { + this.snackBar.open(`Unable to decrypt data. Probably wrong password. Try again.`, 'Hide', { + duration: 3000, + horizontalPosition: 'center', + verticalPosition: 'bottom', + }); + } + } + }); + } + private initThreadProfiles() { + for (let i = 0; i < 10; i++) { + this.wallet.getThread(i); + } + this.privateThreads.next(this.wallet.threads); + } + async updateThreadMetadata(thread: PrivateThread) { + let event = this.dataService.createEventWithPubkey(17761, JSON.stringify(thread.p), thread.ap.publicKey.toString('hex')); + thread!.p.created_at = event.created_at; + event.content = JSON.stringify(thread.p); + const signature = signEvent(event, thread.ap.privateKey.toString('hex')) as any; + const signedEvent = event as Event; + signedEvent.sig = signature; + signedEvent.id = await getEventHash(event); + await this.dataService.publishEvent(signedEvent); + return true; + } + + async saveNote(thread: PrivateThread, noteText: string) { + const postDocument = PostDocument.default; + postDocument.p = { + message: noteText + } + const index = thread.p.last_known_index + 1; + const ap = thread.indexMap.post.ap.deriveChildKey(index); + const sp = thread.indexMap.post.sp.deriveChildKey(index); + const address = new HDKissAddress({ publicKey: ap.publicKey, type: HDKissDocumentType.Post, version: Versions.animiqAPI3 }); + const encrypted = address.encrypt(JSON.stringify(postDocument.p), sp.publicKey, 1); + let event = this.dataService.createEventWithPubkey(17761, encrypted, ap.publicKey.toString('hex')); + const signature = signEvent(event, ap.privateKey.toString('hex')) as any; + const signedEvent = event as Event; + signedEvent.sig = signature; + signedEvent.id = await getEventHash(event); + await this.dataService.publishEvent(signedEvent); + thread.p.last_known_index = index; + await this.updateThreadMetadata(thread); + return true; + } +} diff --git a/src/app/shared/private-threads/private-threads.component.html b/src/app/shared/private-threads/private-threads.component.html new file mode 100644 index 00000000..7d1c2d45 --- /dev/null +++ b/src/app/shared/private-threads/private-threads.component.html @@ -0,0 +1,91 @@ +
+ + +
+ Back to Edit Profile + Manage Private Threads + Randomize + Save + Add Thread +
+

NIP 76 Private Threads are not initialized for this profile yet.

+

To get started:

+
    +
  1. Randomize the thread profile + keys. (OPTIONAL).
  2. +
  3. Save it to begin using this + thread profile.
  4. +
+
+
+
+ + +
+ + + + +
+
+ + Index + + + + Name + + + + Description + + +
+
+ + Index + + + + Name + + + + Description + + +
+
+
+
+
+
+
+
+ + What's on your mind? + + + + sentiment_satisfied +
+
+   + +
+
+ + +
+ + {{ post.nostrEvent.created_at | ago }} + + + +
+ +
+ +
+
+
\ No newline at end of file diff --git a/src/app/shared/private-threads/private-threads.component.scss b/src/app/shared/private-threads/private-threads.component.scss new file mode 100644 index 00000000..a58364c0 --- /dev/null +++ b/src/app/shared/private-threads/private-threads.component.scss @@ -0,0 +1,33 @@ + +.menu-link { + display: inline-block; + padding-right: 16px; + padding-bottom: 8px; + cursor: pointer; +} +.action-link { + display: inline-block; + cursor: pointer; +} +li.instruction { + padding-bottom: 16px; +} +.rounded-button { + border-radius: 16px; + margin-top: 0.24em; + margin-right: 0.5em; + margin-bottom: 12px; +} +.thread-toolbar { + text-align: right;; +} +.thread-card{ + margin-bottom: 16px; +} +.thread-card.edit { + background-color: aliceblue; +} +.input-full-width { + width: 100%; + background-color: white; +} \ No newline at end of file diff --git a/src/app/shared/private-threads/private-threads.component.spec.ts b/src/app/shared/private-threads/private-threads.component.spec.ts new file mode 100644 index 00000000..6cf1c496 --- /dev/null +++ b/src/app/shared/private-threads/private-threads.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PrivateThreadsComponent } from './private-threads.component'; + +describe('PrivateThreadsComponent', () => { + let component: PrivateThreadsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ PrivateThreadsComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PrivateThreadsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/private-threads/private-threads.component.ts b/src/app/shared/private-threads/private-threads.component.ts new file mode 100644 index 00000000..919621a8 --- /dev/null +++ b/src/app/shared/private-threads/private-threads.component.ts @@ -0,0 +1,223 @@ +import { Component, ViewChild } from '@angular/core'; +import { FormBuilder, FormControl, Validators } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { HDKey, HDKissAddress, HDKissDocumentType, IThreadPayload, PostDocument, PrivateThread, Versions } from 'animiq-nip76-tools'; +import { Subject } from 'rxjs'; +import { RelayService } from 'src/app/services/relay'; +import { ApplicationState } from '../../services/applicationstate'; +import { NostrEvent, NostrProfileDocument, NostrRelaySubscription } from '../../services/interfaces'; +import { NavigationService } from '../../services/navigation'; +import { Nip76Service } from '../../services/nip76.service'; +import { ProfileService } from '../../services/profile'; +import { UIService } from '../../services/ui'; +@Component({ + selector: 'app-private-threads', + templateUrl: './private-threads.component.html', + styleUrls: ['./private-threads.component.scss'] +}) +export class PrivateThreadsComponent { + profile?: NostrProfileDocument; + privateThreads: PrivateThread[] = []; + privateThreadsSubId = '' + privateNotesSubId = '' + + private _editThread: PrivateThread | null = null; + + activeThreadPubKey: string | null = null; + activeThread: PrivateThread | undefined; + isEmojiPickerVisible = false; + @ViewChild('picker') picker: unknown; + @ViewChild('noteContent') noteContent?: FormControl; + noteForm = this.fb.group({ + content: ['', Validators.required], + expiration: [''], + dateControl: [], + }); + + constructor( + private router: Router, + private activatedRoute: ActivatedRoute, + public navigation: NavigationService, + public appState: ApplicationState, + public ui: UIService, + private profileService: ProfileService, + private relayService: RelayService, + public nip76Service: Nip76Service, + private fb: FormBuilder, + ) { } + + async ngOnInit() { + this.activatedRoute.paramMap.subscribe(async (params) => { + this.activeThreadPubKey = params.get('threadPubKey'); + const thread = this.nip76Service.wallet.threads.find(x => this.activeThreadPubKey === x.ap.publicKey.toString('hex')); + if (thread) this.initNotes(thread); + }); + + this.profileService.profile$.subscribe((profile) => { + this.profile = profile; + this.ui.setProfile(this.profile); + }); + + if (this.nip76Service.wallet.isGuest) { + this.randomize(); + } else { + this.nip76Service.privateThreads.subscribe(threads => { + this.initThreads(threads); + }); + } + } + + initThreads(threads: PrivateThread[]) { + if (!this.nip76Service.wallet.isGuest && threads.length) { + + this.privateThreads = threads; + if (this.privateThreadsSubId) { + this.relayService.unsubscribe(this.privateThreadsSubId); + } + + const privateThreads$ = new Subject(); + privateThreads$.subscribe(nostrEvent => { + const thread = this.nip76Service.wallet.threads.find(x => nostrEvent.pubkey === x.ap.publicKey.toString('hex')); + if (thread) { + thread.ownerPubKey = this.profile?.pubkey!; + thread.p = JSON.parse(nostrEvent.content) as IThreadPayload; + thread.ready = true; + } + }); + + const threadPubKeys = this.nip76Service.wallet.threads.map(x => x.ap.publicKey.toString('hex')); + this.privateThreadsSubId = this.relayService.subscribe([{ + authors: threadPubKeys, + kinds: [17761], + limit: 100 + }], 'nip76.privateThreads.List', 'Replaceable', privateThreads$).id; + } + } + + initNotes(thread: PrivateThread | undefined, indexStart = 0) { + this.activeThread = thread; + if (!this.nip76Service.wallet.isGuest && this.activeThread) { + if (this.privateNotesSubId) { + this.relayService.unsubscribe(this.privateNotesSubId); + } + const privateNotes$ = new Subject(); + privateNotes$.subscribe(nostrEvent => { + let keyIndex = 0; + let ap: HDKey; + let sp: HDKey | undefined; + for (let i = indexStart; i < indexStart + 10; i++) { + ap = this.activeThread!.indexMap.post.ap.deriveChildKey(i); + if (nostrEvent.pubkey === ap.publicKey.toString('hex')) { + keyIndex = i; + sp = this.activeThread!.indexMap.post.sp.deriveChildKey(i); + break; + } + } + if (sp) { + const post = PostDocument.default; + post.setKeys(ap!, sp!); + post.address = new HDKissAddress({ publicKey: ap!.publicKey, type: HDKissDocumentType.Post, version: Versions.animiqAPI3 }); + post.thread = this.activeThread!; + post.s = nostrEvent.sig; + post.h = nostrEvent.id; + post.a = post.a = post.address.value; + post.v = 3; + post.e = nostrEvent.content; + post.i = keyIndex; + post.t = nostrEvent.created_at; + // this.activeThread?.indexMap.post.decrypt(post); + const decrypted = post.address.decrypt(post.e, sp.publicKey, post.v); + post.p = JSON.parse(decrypted); + nostrEvent.content = post.p.message!; + post.nostrEvent = nostrEvent; + this.activeThread!.posts.push(post); + } + }); + + const notePubKeys: string[] = []; + for (let i = indexStart; i < indexStart + 10; i++) { + const ap = this.activeThread.indexMap.post.ap.deriveChildKey(i); + notePubKeys.push(ap.publicKey.toString('hex')); + } + this.privateNotesSubId = this.relayService.subscribe([{ + authors: notePubKeys, + kinds: [17761], + limit: 100 + }], `nip76.privateNotes.${this.activeThread.a}.List`, 'Replaceable', privateNotes$).id; + } + } + + public trackByFn(index: number, item: PostDocument) { + return item.a; + } + + randomize() { + this.nip76Service.wallet.reKey(); + this.privateThreads = [this.nip76Service.wallet.threads[0]]; + this._editThread = this.privateThreads[0]; + this.privateThreads[0].pending = 'preview'; + this.privateThreads[0].p.name = 'Example Thread 1'; + this.privateThreads[0].p.description = 'My First Private Trace Resistant Thread 1'; + this.privateThreads[0].ready = true; + } + + async saveKey() { + const savedLocal = await this.nip76Service.saveKeyWithPassword(); + const savedRemote = await this.nip76Service.updateThreadMetadata(this.privateThreads[0]); + // location.reload(); + } + + get editThread(): PrivateThread | null { + return this._editThread!; + } + set editThread(value: PrivateThread | null) { + this.cancelEdit(); + this._editThread = value; + } + + viewThread(thread: PrivateThread) { + this.router.navigate(['profile', 'private-threads', thread.ap.publicKey.toString('hex')]); + } + + addThread() { + this.cancelEdit(); + const firstAvailable = this.privateThreads.find(x => !x.ready); + if (firstAvailable) { + firstAvailable.pending = firstAvailable.a; + firstAvailable.ready = true; + firstAvailable.p.name = 'New Thread'; + this._editThread = firstAvailable; + } + } + + cancelEdit() { + if (this._editThread && this._editThread.pending === this._editThread.a) { + this._editThread.pending = ''; + this._editThread.ready = false; + } + this._editThread = null; + } + + async saveThread() { + this._editThread!.pending = ''; + const savedRemote = await this.nip76Service.updateThreadMetadata(this._editThread!); + if (savedRemote) { + this._editThread = null; + } + } + + public addEmojiNote(event: { emoji: { native: any } }) { + let startPos = (this.noteContent).nativeElement.selectionStart; + let value = this.noteForm.controls.content.value; + + let parsedValue = value?.substring(0, startPos) + event.emoji.native + value?.substring(startPos, value.length); + this.noteForm.controls.content.setValue(parsedValue); + this.isEmojiPickerVisible = false; + + (this.noteContent).nativeElement.focus(); + } + + async saveNote() { + await this.nip76Service.saveNote(this.activeThread!, this.noteForm.controls.content.value!); + } +} diff --git a/tsconfig.json b/tsconfig.json index 892ba2fd..6c40477f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,7 +23,18 @@ "ES2022", "ESNext", "dom" - ] + ], + "paths": { + "animiq-lib": [ + "../../animiq-lib/dist/animiq-lib" + ], + "animiq-nip76-tools": [ + "../../animiq-nip76-tools/dist/" + ], + "@angular/*": [ + "./node_modules/@angular/*" + ] + }, }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, From 434875d822aa5d9f0069bdfef9f95cbaae94833e Mon Sep 17 00:00:00 2001 From: d-krause Date: Wed, 8 Mar 2023 12:02:46 -0500 Subject: [PATCH 04/39] basic key sharing , viewing other peoples private threads --- src/app/app-routing.module.ts | 29 +- src/app/app.html | 4 + src/app/app.module.ts | 6 +- .../add-thread-dialog.component.html | 16 + .../add-thread-dialog.component.scss | 0 .../add-thread-dialog.component.spec.ts | 23 ++ .../add-thread-dialog.component.ts | 20 ++ .../nip76-settings.component.html | 140 +++++++++ .../nip76-settings.component.scss} | 30 +- .../nip76-settings.component.spec.ts} | 10 +- .../nip76-settings.component.ts | 181 +++++++++++ .../{services => nip76}/nip76.service.spec.ts | 0 src/app/nip76/nip76.service.ts | 285 ++++++++++++++++++ src/app/services/nip76.service.ts | 146 --------- .../private-threads.component.html | 91 ------ .../private-threads.component.ts | 223 -------------- 16 files changed, 725 insertions(+), 479 deletions(-) create mode 100644 src/app/nip76/add-thread-dialog/add-thread-dialog.component.html create mode 100644 src/app/nip76/add-thread-dialog/add-thread-dialog.component.scss create mode 100644 src/app/nip76/add-thread-dialog/add-thread-dialog.component.spec.ts create mode 100644 src/app/nip76/add-thread-dialog/add-thread-dialog.component.ts create mode 100644 src/app/nip76/nip76-settings/nip76-settings.component.html rename src/app/{shared/private-threads/private-threads.component.scss => nip76/nip76-settings/nip76-settings.component.scss} (59%) rename src/app/{shared/private-threads/private-threads.component.spec.ts => nip76/nip76-settings/nip76-settings.component.spec.ts} (56%) create mode 100644 src/app/nip76/nip76-settings/nip76-settings.component.ts rename src/app/{services => nip76}/nip76.service.spec.ts (100%) create mode 100644 src/app/nip76/nip76.service.ts delete mode 100644 src/app/services/nip76.service.ts delete mode 100644 src/app/shared/private-threads/private-threads.component.html delete mode 100644 src/app/shared/private-threads/private-threads.component.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 6237ea02..873de9e6 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -33,7 +33,7 @@ import { LoginComponent } from './connect/login/login'; import { CreateProfileComponent } from './connect/create/create'; import { EditorBadgesComponent } from './editor-badges/editor'; import { BadgeComponent } from './badge/badge'; -import { PrivateThreadsComponent } from './shared/private-threads/private-threads.component'; +import { Nip76SettingsComponent } from './nip76/nip76-settings/nip76-settings.component'; const routes: Routes = [ { @@ -141,17 +141,36 @@ const routes: Routes = [ }, }, { - path: 'profile/private-threads', - component: PrivateThreadsComponent, + path: 'private-threads', + component: Nip76SettingsComponent, canActivate: [AuthGuard], resolve: { data: LoadingResolverService, }, }, { - path: 'profile/private-threads/:threadPubKey', - component: PrivateThreadsComponent, + path: 'private-threads/following', + component: Nip76SettingsComponent, canActivate: [AuthGuard], + data: { tabIndex: 1 }, + resolve: { + data: LoadingResolverService, + }, + }, + { + path: 'private-threads/:threadPubKey/followers', + component: Nip76SettingsComponent, + data: { tabIndex: 2 }, + canActivate: [AuthGuard], + resolve: { + data: LoadingResolverService, + }, + }, + { + path: 'private-threads/:threadPubKey/notes', + component: Nip76SettingsComponent, + canActivate: [AuthGuard], + data: { tabIndex: 3 }, resolve: { data: LoadingResolverService, }, diff --git a/src/app/app.html b/src/app/app.html index a136180f..fe615934 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -175,6 +175,10 @@

@{{ profile?.name }}

badge Badges + + key + Private Threads + settings Settings diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 67753903..dfd9fe6c 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -151,8 +151,9 @@ import { EditorBadgesComponent } from './editor-badges/editor'; import { BadgeCardComponent } from './shared/badge-card/badge-card'; import { TagsComponent } from './shared/tags/tags'; import { BadgeComponent } from './badge/badge'; -import { PrivateThreadsComponent } from './shared/private-threads/private-threads.component'; import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; +import { Nip76SettingsComponent } from './nip76/nip76-settings/nip76-settings.component'; +import { AddThreadDialog } from './nip76/add-thread-dialog/add-thread-dialog.component'; @NgModule({ declarations: [ @@ -244,7 +245,8 @@ import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; BadgeCardComponent, TagsComponent, BadgeComponent, - PrivateThreadsComponent, + Nip76SettingsComponent, + AddThreadDialog, ], imports: [ AboutModule, diff --git a/src/app/nip76/add-thread-dialog/add-thread-dialog.component.html b/src/app/nip76/add-thread-dialog/add-thread-dialog.component.html new file mode 100644 index 00000000..c508dfab --- /dev/null +++ b/src/app/nip76/add-thread-dialog/add-thread-dialog.component.html @@ -0,0 +1,16 @@ +
+

Thread Preview

+
+ + key + nsecthread + + +
+ +
+ + +
+
+ \ No newline at end of file diff --git a/src/app/nip76/add-thread-dialog/add-thread-dialog.component.scss b/src/app/nip76/add-thread-dialog/add-thread-dialog.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/nip76/add-thread-dialog/add-thread-dialog.component.spec.ts b/src/app/nip76/add-thread-dialog/add-thread-dialog.component.spec.ts new file mode 100644 index 00000000..8b0b5433 --- /dev/null +++ b/src/app/nip76/add-thread-dialog/add-thread-dialog.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AddThreadDialog } from './add-thread-dialog.component'; + +describe('AddThreadDialogComponent', () => { + let component: AddThreadDialog; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ AddThreadDialog ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AddThreadDialog); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/nip76/add-thread-dialog/add-thread-dialog.component.ts b/src/app/nip76/add-thread-dialog/add-thread-dialog.component.ts new file mode 100644 index 00000000..87b15cd6 --- /dev/null +++ b/src/app/nip76/add-thread-dialog/add-thread-dialog.component.ts @@ -0,0 +1,20 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; + +export interface AddThreadDialogData { + threadPointer: string +} + +@Component({ + selector: 'app-add-thread-dialog', + templateUrl: './add-thread-dialog.component.html', + styleUrls: ['./add-thread-dialog.component.scss'] +}) +export class AddThreadDialog { + constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: AddThreadDialogData) {} + + onNoClick(): void { + this.data.threadPointer = ''; + this.dialogRef.close(); + } +} diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.html b/src/app/nip76/nip76-settings/nip76-settings.component.html new file mode 100644 index 00000000..2a474cb4 --- /dev/null +++ b/src/app/nip76/nip76-settings/nip76-settings.component.html @@ -0,0 +1,140 @@ +
+ + + + + +
+ +
+

NIP 76 Private Threads are not initialized for this profile yet.

+

To get started:

+
    +
  1. Randomize the thread profile + keys. (OPTIONAL).
  2. +
  3. Save it to begin using this + thread profile.
  4. +
+ +
+ + Index + + + + Name + + + + Description + + +
+
+
+ + + +
+
+ + +
+ + + + + + +
+
+ + Index + + + + Name + + + + Description + + +
+
+ + Index + + + + Name + + + + Description + + +
+
+
+
+
+
+ +
+
+ + + +
+
+ Private Thread {{activeThread.address.formatted}}
+
+
+
+ +
+
+ Private Thread {{activeThread.address.formatted}}
+ +
+
+
+ + What's on your mind? + + + + sentiment_satisfied +
+
+   + +
+
+ +
+ + {{ post.nostrEvent.created_at | ago }} + + + +
+ +
+
+
+
+ + diff --git a/src/app/shared/private-threads/private-threads.component.scss b/src/app/nip76/nip76-settings/nip76-settings.component.scss similarity index 59% rename from src/app/shared/private-threads/private-threads.component.scss rename to src/app/nip76/nip76-settings/nip76-settings.component.scss index a58364c0..e1103b1d 100644 --- a/src/app/shared/private-threads/private-threads.component.scss +++ b/src/app/nip76/nip76-settings/nip76-settings.component.scss @@ -1,33 +1,49 @@ - .menu-link { display: inline-block; padding-right: 16px; padding-bottom: 8px; cursor: pointer; } + .action-link { display: inline-block; cursor: pointer; } + li.instruction { padding-bottom: 16px; } + .rounded-button { border-radius: 16px; margin-top: 0.24em; margin-right: 0.5em; margin-bottom: 12px; } -.thread-toolbar { - text-align: right;; -} -.thread-card{ - margin-bottom: 16px; + +.settings-card { + margin: 16px 32px 16px 0px; } -.thread-card.edit { + +.settings-card.edit { background-color: aliceblue; } + .input-full-width { width: 100%; background-color: white; +} + +.new-post-button { + // width: 92px; + // height: 92px; + // position: fixed; + // bottom: 2em; + // left: 2.9em; + cursor: pointer; + transition: opacity 250ms ease; +} + +.new-post-button:hover { + opacity: 0.6; } \ No newline at end of file diff --git a/src/app/shared/private-threads/private-threads.component.spec.ts b/src/app/nip76/nip76-settings/nip76-settings.component.spec.ts similarity index 56% rename from src/app/shared/private-threads/private-threads.component.spec.ts rename to src/app/nip76/nip76-settings/nip76-settings.component.spec.ts index 6cf1c496..fc9f294b 100644 --- a/src/app/shared/private-threads/private-threads.component.spec.ts +++ b/src/app/nip76/nip76-settings/nip76-settings.component.spec.ts @@ -1,18 +1,18 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { PrivateThreadsComponent } from './private-threads.component'; +import { Nip76SettingsComponent } from './nip76-settings.component'; describe('PrivateThreadsComponent', () => { - let component: PrivateThreadsComponent; - let fixture: ComponentFixture; + let component: Nip76SettingsComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ PrivateThreadsComponent ] + declarations: [ Nip76SettingsComponent ] }) .compileComponents(); - fixture = TestBed.createComponent(PrivateThreadsComponent); + fixture = TestBed.createComponent(Nip76SettingsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.ts b/src/app/nip76/nip76-settings/nip76-settings.component.ts new file mode 100644 index 00000000..5741faa0 --- /dev/null +++ b/src/app/nip76/nip76-settings/nip76-settings.component.ts @@ -0,0 +1,181 @@ +import { Component, ViewChild } from '@angular/core'; +import { FormBuilder, FormControl, Validators } from '@angular/forms'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { MatTabChangeEvent } from '@angular/material/tabs'; +import { ActivatedRoute, Router } from '@angular/router'; +import { PostDocument, PrivateThread } from 'animiq-nip76-tools'; +import { ApplicationState } from '../../services/applicationstate'; +import { NostrProfileDocument } from '../../services/interfaces'; +import { NavigationService } from '../../services/navigation'; +import { UIService } from '../../services/ui'; +import { Nip76Service } from '../nip76.service'; +@Component({ + selector: 'app-nip76-settings', + templateUrl: './nip76-settings.component.html', + styleUrls: ['./nip76-settings.component.scss'] +}) +export class Nip76SettingsComponent { + tabIndex?: number; + showNoteForm = false; + private _editThread: PrivateThread | null = null; + activeThread: PrivateThread | undefined; + isEmojiPickerVisible = false; + @ViewChild('picker') picker: unknown; + @ViewChild('noteContent') noteContent?: FormControl; + noteForm = this.fb.group({ + content: ['', Validators.required], + expiration: [''], + dateControl: [], + }); + + constructor( + private router: Router, + private activatedRoute: ActivatedRoute, + private snackBar: MatSnackBar, + public navigation: NavigationService, + public appState: ApplicationState, + public ui: UIService, + public nip76Service: Nip76Service, + private fb: FormBuilder, + ) { } + + get profile(): NostrProfileDocument { + return this.nip76Service.profile; + } + + async ngOnInit() { + if (this.nip76Service.wallet.isGuest) { + this.randomizeKey(); + } + this.activatedRoute.paramMap.subscribe(async (params) => { + this.tabIndex = this.activatedRoute.snapshot.data['tabIndex'] as number || 0; + const activeThreadPubKey = params.get('threadPubKey'); + const thread = this.nip76Service.threads.find(x => activeThreadPubKey === x.ap.publicKey.slice(1).toString('hex')); + if (thread) { + this.activeThread = thread; + if (this.tabIndex < 2) this.tabIndex = 3; + if (this.tabIndex === 3 && !thread.sub) { + this.nip76Service.loadNotes(this.activeThread); + } + } + }); + } + + public trackByFn(index: number, item: PostDocument) { + return item.a; + } + + randomizeKey() { + this.nip76Service.wallet.reKey(); + this.editThread = this.nip76Service.wallet.threads[0]; + this.editThread.pending = 'preview'; + this.editThread.p.name = 'Example Thread 1'; + this.editThread.p.description = 'My First Private Trace Resistant Thread 1'; + this.editThread.ready = true; + } + + async saveConfiguration() { + const savedLocal = await this.nip76Service.save(); + const savedRemote = await this.nip76Service.updateThreadMetadata(this.editThread!); + location.reload(); + } + + get editThread(): PrivateThread | null { + return this._editThread!; + } + set editThread(value: PrivateThread | null) { + this.cancelEdit(); + this._editThread = value; + } + + onTabChanged(event: MatTabChangeEvent) { + this.tabIndex = event.index; + switch (event.index) { + case 0: + this.router.navigate(['/private-threads']); + break; + case 1: + this.router.navigate(['/private-threads/following']); + break; + case 2: + this.viewThreadFollowers(this.nip76Service.threads[0]); + break; + case 3: + this.viewThreadNotes(this.nip76Service.threads[0]); + break; + } + } + + viewThreadNotes(thread: PrivateThread) { + this.router.navigate(['private-threads', thread.ap.publicKey.slice(1).toString('hex'), 'notes']); + } + + viewThreadFollowers(thread: PrivateThread) { + this.router.navigate(['private-threads', thread.ap.publicKey.slice(1).toString('hex'), 'followers']); + } + + copyKeys(thread: PrivateThread) { + navigator.clipboard.writeText(thread.thread); + this.snackBar.open(`Thread keys are now in your clipboard.`, 'Hide', { + duration: 3000, + horizontalPosition: 'center', + verticalPosition: 'bottom', + }); + } + + async previewThread() { + const thread = await this.nip76Service.previewThread(); + if (thread) { + if (this.tabIndex != 3) { + this.viewThreadNotes(thread); + } else { + this.activeThread = thread; + } + } + } + + addThread() { + this.cancelEdit(); + const firstAvailable = this.nip76Service.wallet.threads.find(x => !x.ready); + if (firstAvailable) { + firstAvailable.pending = firstAvailable.a; + firstAvailable.ready = true; + firstAvailable.p.name = 'New Thread'; + this._editThread = firstAvailable; + } + } + + cancelEdit() { + if (this._editThread && this._editThread.pending === this._editThread.a) { + this._editThread.pending = ''; + this._editThread.ready = false; + } + this._editThread = null; + } + + async saveThread() { + this._editThread!.pending = ''; + const savedRemote = await this.nip76Service.updateThreadMetadata(this._editThread!); + if (savedRemote) { + this._editThread = null; + } + } + + public addEmojiNote(event: { emoji: { native: any } }) { + let startPos = (this.noteContent).nativeElement.selectionStart; + let value = this.noteForm.controls.content.value; + + let parsedValue = value?.substring(0, startPos) + event.emoji.native + value?.substring(startPos, value.length); + this.noteForm.controls.content.setValue(parsedValue); + this.isEmojiPickerVisible = false; + + (this.noteContent).nativeElement.focus(); + } + + async saveNote() { + if (await this.nip76Service.saveNote(this.activeThread!, this.noteForm.controls.content.value!)) { + this.noteForm.reset(); + this.showNoteForm = false; + } + } +} diff --git a/src/app/services/nip76.service.spec.ts b/src/app/nip76/nip76.service.spec.ts similarity index 100% rename from src/app/services/nip76.service.spec.ts rename to src/app/nip76/nip76.service.spec.ts diff --git a/src/app/nip76/nip76.service.ts b/src/app/nip76/nip76.service.ts new file mode 100644 index 00000000..ee85d3e4 --- /dev/null +++ b/src/app/nip76/nip76.service.ts @@ -0,0 +1,285 @@ +import { Injectable } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { HDKey, HDKissAddress, HDKissDocumentType, IThreadPayload, nip19Extension, Nip76Wallet, PostDocument, PrivateThread, Versions } from 'animiq-nip76-tools'; +import { Event, getEventHash, signEvent } from 'nostr-tools'; +import { Subject } from 'rxjs'; +import { DataService } from '../services/data'; +import { NostrEvent, NostrProfileDocument, NostrRelaySubscription } from '../services/interfaces'; +import { ProfileService } from '../services/profile'; +import { RelayService } from '../services/relay'; +import { SecurityService } from '../services/security'; +import { PasswordDialog, PasswordDialogData } from '../shared/password-dialog/password-dialog'; +import { AddThreadDialog, AddThreadDialogData } from './add-thread-dialog/add-thread-dialog.component'; + +const sessionKeyAddress = 'blockcore:notes:nostr:nip76:sessionKey'; +const nostrPrivKeyAddress = 'blockcore:notes:nostr:prvkey'; + +interface PrivateThreadWithRelaySub extends PrivateThread { + sub?: NostrRelaySubscription; +} + +@Injectable({ + providedIn: 'root' +}) +export class Nip76Service { + + wallet: Nip76Wallet; + profile!: NostrProfileDocument; + threads: PrivateThreadWithRelaySub[] = []; + + constructor( + private dialog: MatDialog, + private snackBar: MatSnackBar, + private security: SecurityService, + private profileService: ProfileService, + private relayService: RelayService, + private dataService: DataService + ) { + this.profileService.profile$.subscribe((profile) => { + this.profile = profile!; + }); + this.wallet = new Nip76Wallet(); + if (this.wallet.isInSession) { + const sessionKey = sessionStorage.getItem(sessionKeyAddress); + if (sessionKey) { + if (this.wallet.readKey(sessionKey, 'session', '')) { + this.initWalletThreads(); + } else { + sessionStorage.removeItem(sessionKeyAddress); + this.wallet.clearSession(); + } + } else { + this.login(); + } + } else if (this.wallet.requiresLogin) { + this.login(); + } + } + + private async passwordDialog(actionPrompt: string, onSuccess: (key: string) => void): Promise { + return new Promise((resolve, reject) => { + const dialogRef = this.dialog.open(PasswordDialog, { + data: { action: actionPrompt, password: '' }, + maxWidth: '100vw', + panelClass: 'full-width-dialog', + }); + dialogRef.afterClosed().subscribe(async (result: PasswordDialogData) => { + if (result) { + const prvkeyEncrypted = localStorage.getItem(nostrPrivKeyAddress); + const prvkey = await this.security.decryptData(prvkeyEncrypted!, result.password); + if (prvkey) { + onSuccess.apply(this, [prvkey]); + resolve(true); + } else { + this.snackBar.open(`Unable to decrypt data. Probably wrong password. Try again.`, 'Hide', { + duration: 3000, + horizontalPosition: 'center', + verticalPosition: 'bottom', + }); + reject(); + } + } else { + reject(); + } + }); + }); + } + + private initWalletThreads() { + for (let i = 0; i < 10; i++) { + this.wallet.getThread(i); + this.wallet.threads[i].ownerPubKey = this.profile.pubkey; + } + this.threads = [...this.threads, ...this.wallet.threads]; + this.loadThreads(); + } + + private saveWallet(password: string) { + this.wallet.saveKey(password, 'backup', false); + const sessionKey = this.wallet.generateSessionKey(); + sessionStorage.setItem(sessionKeyAddress, sessionKey); + this.wallet.saveKey(sessionKey, 'session', false); + this.wallet.isInSession = true; + this.initWalletThreads(); + } + + private loadWallet(password: string) { + if (this.wallet.readKey(password, 'backup', '')) { + this.saveWallet(password); + } + } + + async previewThread(): Promise { + return new Promise((resolve, reject) => { + const dialogRef = this.dialog.open(AddThreadDialog, { + data: { threadPointer: '' }, + maxWidth: '200vw', + panelClass: 'full-width-dialog', + }); + dialogRef.afterClosed().subscribe(async (result: AddThreadDialogData) => { + if (result?.threadPointer) { + const pointer = nip19Extension.decode(result.threadPointer); + if (pointer) { + const thread = PrivateThread.fromPointer(pointer.data as nip19Extension.SecureThreadPointer); + this.threads = [...this.threads]; + const threadIndex = this.threads.findIndex(x => x.a == thread.a); + if (threadIndex === -1) { + this.threads.push(thread); + } else { + if(this.threads[threadIndex].sub){ + this.relayService.unsubscribe(this.threads[threadIndex].sub!.id); + } + this.threads[threadIndex] = thread; + } + this.loadThreads(); + this.loadNotes(thread); + resolve(thread); + } else { + this.snackBar.open(`Unable to decrypt data. Probably wrong password. Try again.`, 'Hide', { + duration: 3000, + horizontalPosition: 'center', + verticalPosition: 'bottom', + }); + reject(); + } + } else { + reject(); + } + }); + }); + } + + async save(): Promise { + return this.passwordDialog('Save Private Thread Keys', this.saveWallet); + } + + async login(): Promise { + return this.passwordDialog('Load Private Thread Keys', this.loadWallet); + } + + async logout() { + sessionStorage.removeItem(sessionKeyAddress); + this.wallet.clearSession(); + this.wallet = new Nip76Wallet(); + } + + private loadThreadsSubId = '' + + loadThreads() { + if (this.threads.length) { + if (this.loadThreadsSubId) { + this.relayService.unsubscribe(this.loadThreadsSubId); + } + const privateThreads$ = new Subject(); + privateThreads$.subscribe(nostrEvent => { + const thread = this.threads.find(x => nostrEvent.pubkey === x.ap.publicKey.slice(1).toString('hex')); + if (thread) { + thread.p = JSON.parse(nostrEvent.content) as IThreadPayload; + thread.ready = true; + } + }); + const threadPubKeys = this.threads.map(x => x.ap.publicKey.slice(1).toString('hex')); + this.loadThreadsSubId = this.relayService.subscribe([{ + authors: threadPubKeys, + kinds: [17761], + limit: 100 + }], `nip76Service.loadThreads`, 'Replaceable', privateThreads$).id; + } + } + + loadNotes(thread: PrivateThreadWithRelaySub | undefined, indexStart = 0) { + if (thread) { + if (thread.sub) { + this.relayService.unsubscribe(thread.sub.id); + } + const privateNotes$ = new Subject(); + privateNotes$.subscribe(nostrEvent => { + let keyIndex = 0; + let ap: HDKey; + let sp: HDKey | undefined; + for (let i = indexStart; i < indexStart + 10; i++) { + ap = thread!.indexMap.post.ap.deriveChildKey(i); + if (nostrEvent.pubkey === ap.publicKey.slice(1).toString('hex')) { + keyIndex = i; + sp = thread!.indexMap.post.sp.deriveChildKey(i); + break; + } + } + if (sp) { + const postIndex = thread.posts.findIndex(x => x.h === nostrEvent.id); + if (postIndex === -1 || thread.posts[postIndex].t < nostrEvent.created_at) { + const post = PostDocument.default; + post.setKeys(ap!, sp!); + post.address = new HDKissAddress({ publicKey: ap!.publicKey, type: HDKissDocumentType.Post, version: Versions.animiqAPI3 }); + post.thread = thread; + post.s = nostrEvent.sig; + post.h = nostrEvent.id; + post.a = post.a = post.address.value; + post.v = 3; + post.e = nostrEvent.content; + post.i = keyIndex; + post.t = nostrEvent.created_at; + // thread?.indexMap.post.decrypt(post); + const decrypted = post.address.decrypt(post.e, sp.publicKey, post.v); + post.p = JSON.parse(decrypted); + if (post.p) nostrEvent.content = post.p.message!; + post.ownerPubKey = thread.ownerPubKey; + post.nostrEvent = nostrEvent; + // console.log(`decrypt - ${post.nostrEvent.content.substring(0,10)}`, Array.from(sp.publicKey)) + if (postIndex === -1) { + thread.posts.push(post); + } else { + thread.posts[postIndex] = post; + } + thread.posts = thread.posts.sort((a, b) => b.t - a.t); + } + } + }); + + const notePubKeys: string[] = []; + for (let i = indexStart; i < indexStart + 10; i++) { + const ap = thread.indexMap.post.ap.deriveChildKey(i); + notePubKeys.push(ap.publicKey.slice(1).toString('hex')); + } + thread.sub = this.relayService.subscribe([{ + authors: notePubKeys, + kinds: [17761], + limit: 100 + }], `nip76Service.loadNotes.${thread.a}`, 'Replaceable', privateNotes$); + } + } + + async updateThreadMetadata(thread: PrivateThreadWithRelaySub) { + let event = this.dataService.createEventWithPubkey(17761, JSON.stringify(thread.p), thread.ap.publicKey.slice(1).toString('hex')); + thread!.p.created_at = event.created_at; + event.content = JSON.stringify(thread.p); + const signature = signEvent(event, thread.ap.privateKey.toString('hex')) as any; + const signedEvent = event as Event; + signedEvent.sig = signature; + signedEvent.id = await getEventHash(event); + await this.dataService.publishEvent(signedEvent); + return true; + } + + async saveNote(thread: PrivateThreadWithRelaySub, noteText: string) { + const postDocument = PostDocument.default; + postDocument.p = { + message: noteText + } + const index = thread.p.last_known_index + 1; + const ap = thread.indexMap.post.ap.deriveChildKey(index); + const sp = thread.indexMap.post.sp.deriveChildKey(index); + const address = new HDKissAddress({ publicKey: ap.publicKey, type: HDKissDocumentType.Post, version: Versions.animiqAPI3 }); + const encrypted = address.encrypt(JSON.stringify(postDocument.p), sp.publicKey, 1); + let event = this.dataService.createEventWithPubkey(17761, encrypted, ap.publicKey.slice(1).toString('hex')); + const signature = signEvent(event, ap.privateKey.toString('hex')) as any; + const signedEvent = event as Event; + signedEvent.sig = signature; + signedEvent.id = await getEventHash(event); + await this.dataService.publishEvent(signedEvent); + thread.p.last_known_index = index; + await this.updateThreadMetadata(thread); + return true; + } +} diff --git a/src/app/services/nip76.service.ts b/src/app/services/nip76.service.ts deleted file mode 100644 index d863da36..00000000 --- a/src/app/services/nip76.service.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { Injectable } from '@angular/core'; -import { MatDialog } from '@angular/material/dialog'; -import { MatSnackBar } from '@angular/material/snack-bar'; -import { HDKey, PrivateThread, Versions, Nip76Wallet, HDKissAddress, HDKissDocumentType, nip19Extension, IThreadPayload, PostDocument } from 'animiq-nip76-tools'; -import { Event, getEventHash, signEvent, validateEvent } from 'nostr-tools'; -import { BehaviorSubject } from 'rxjs'; -import { PasswordDialog, PasswordDialogData } from '../shared/password-dialog/password-dialog'; -import { DataService } from './data'; -import { SecurityService } from './security'; -`` -const sessionKeyAddress = 'blockcore:notes:nostr:nip76:sessionKey'; -const nostrPrivKeyAddress = 'blockcore:notes:nostr:prvkey'; - -@Injectable({ - providedIn: 'root' -}) -export class Nip76Service { - wallet: Nip76Wallet; - privateThreads = new BehaviorSubject([]); - constructor( - private dialog: MatDialog, - private snackBar: MatSnackBar, - private security: SecurityService, - private dataService: DataService - ) { - this.wallet = new Nip76Wallet(); - if (this.wallet.isInSession) { - const sessionKey = sessionStorage.getItem(sessionKeyAddress); - if (sessionKey) { - if (this.wallet.readKey(sessionKey, 'session', '')) { - this.initThreadProfiles(); - } else { - sessionStorage.removeItem(sessionKeyAddress); - this.wallet.clearSession(); - } - } else { - this.loadKeyFromPassword(); - } - } else if (this.wallet.requiresLogin) { - this.loadKeyFromPassword(); - } - } - private save(password: string) { - this.wallet.saveKey(password, 'backup', false); - const sessionKey = this.wallet.generateSessionKey(); - sessionStorage.setItem(sessionKeyAddress, sessionKey); - this.wallet.saveKey(sessionKey, 'session', false); - } - async saveKeyWithPassword(): Promise { - return new Promise((resolve, reject) => { - const dialogRef = this.dialog.open(PasswordDialog, { - data: { action: 'Save Private Thread Keys', password: '' }, - maxWidth: '100vw', - panelClass: 'full-width-dialog', - }); - dialogRef.afterClosed().subscribe(async (result: PasswordDialogData) => { - if (result) { - const prvkeyEncrypted = localStorage.getItem(nostrPrivKeyAddress); - const prvkey = await this.security.decryptData(prvkeyEncrypted!, result.password); - if (prvkey) { - this.save(prvkey); - resolve(true); - } else { - this.snackBar.open(`Unable to decrypt data. Probably wrong password. Try again.`, 'Hide', { - duration: 3000, - horizontalPosition: 'center', - verticalPosition: 'bottom', - }); - reject(); - } - } else { - reject(); - } - }); - }); - } - loadKeyFromPassword() { - const dialogRef = this.dialog.open(PasswordDialog, { - data: { action: 'Load Private Thread Keys', password: '' }, - maxWidth: '100vw', - panelClass: 'full-width-dialog', - }); - dialogRef.afterClosed().subscribe(async (result: PasswordDialogData) => { - if (result) { - const prvkeyEncrypted = localStorage.getItem(nostrPrivKeyAddress); - const prvkey = await this.security.decryptData(prvkeyEncrypted!, result.password); - if (prvkey) { - if (this.wallet.readKey(prvkey, 'backup', '')) { - this.save(prvkey); - this.initThreadProfiles(); - } else { - this.snackBar.open(`Unable to decrypt nip76 data. Probably wrong password. Try again.`, 'Hide', { - duration: 3000, - horizontalPosition: 'center', - verticalPosition: 'bottom', - }); - } - } else { - this.snackBar.open(`Unable to decrypt data. Probably wrong password. Try again.`, 'Hide', { - duration: 3000, - horizontalPosition: 'center', - verticalPosition: 'bottom', - }); - } - } - }); - } - private initThreadProfiles() { - for (let i = 0; i < 10; i++) { - this.wallet.getThread(i); - } - this.privateThreads.next(this.wallet.threads); - } - async updateThreadMetadata(thread: PrivateThread) { - let event = this.dataService.createEventWithPubkey(17761, JSON.stringify(thread.p), thread.ap.publicKey.toString('hex')); - thread!.p.created_at = event.created_at; - event.content = JSON.stringify(thread.p); - const signature = signEvent(event, thread.ap.privateKey.toString('hex')) as any; - const signedEvent = event as Event; - signedEvent.sig = signature; - signedEvent.id = await getEventHash(event); - await this.dataService.publishEvent(signedEvent); - return true; - } - - async saveNote(thread: PrivateThread, noteText: string) { - const postDocument = PostDocument.default; - postDocument.p = { - message: noteText - } - const index = thread.p.last_known_index + 1; - const ap = thread.indexMap.post.ap.deriveChildKey(index); - const sp = thread.indexMap.post.sp.deriveChildKey(index); - const address = new HDKissAddress({ publicKey: ap.publicKey, type: HDKissDocumentType.Post, version: Versions.animiqAPI3 }); - const encrypted = address.encrypt(JSON.stringify(postDocument.p), sp.publicKey, 1); - let event = this.dataService.createEventWithPubkey(17761, encrypted, ap.publicKey.toString('hex')); - const signature = signEvent(event, ap.privateKey.toString('hex')) as any; - const signedEvent = event as Event; - signedEvent.sig = signature; - signedEvent.id = await getEventHash(event); - await this.dataService.publishEvent(signedEvent); - thread.p.last_known_index = index; - await this.updateThreadMetadata(thread); - return true; - } -} diff --git a/src/app/shared/private-threads/private-threads.component.html b/src/app/shared/private-threads/private-threads.component.html deleted file mode 100644 index 7d1c2d45..00000000 --- a/src/app/shared/private-threads/private-threads.component.html +++ /dev/null @@ -1,91 +0,0 @@ -
- - -
- Back to Edit Profile - Manage Private Threads - Randomize - Save - Add Thread -
-

NIP 76 Private Threads are not initialized for this profile yet.

-

To get started:

-
    -
  1. Randomize the thread profile - keys. (OPTIONAL).
  2. -
  3. Save it to begin using this - thread profile.
  4. -
-
-
-
- - -
- - - - -
-
- - Index - - - - Name - - - - Description - - -
-
- - Index - - - - Name - - - - Description - - -
-
-
-
-
-
-
-
- - What's on your mind? - - - - sentiment_satisfied -
-
-   - -
-
- - -
- - {{ post.nostrEvent.created_at | ago }} - - - -
- -
- -
-
-
\ No newline at end of file diff --git a/src/app/shared/private-threads/private-threads.component.ts b/src/app/shared/private-threads/private-threads.component.ts deleted file mode 100644 index 919621a8..00000000 --- a/src/app/shared/private-threads/private-threads.component.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { Component, ViewChild } from '@angular/core'; -import { FormBuilder, FormControl, Validators } from '@angular/forms'; -import { ActivatedRoute, Router } from '@angular/router'; -import { HDKey, HDKissAddress, HDKissDocumentType, IThreadPayload, PostDocument, PrivateThread, Versions } from 'animiq-nip76-tools'; -import { Subject } from 'rxjs'; -import { RelayService } from 'src/app/services/relay'; -import { ApplicationState } from '../../services/applicationstate'; -import { NostrEvent, NostrProfileDocument, NostrRelaySubscription } from '../../services/interfaces'; -import { NavigationService } from '../../services/navigation'; -import { Nip76Service } from '../../services/nip76.service'; -import { ProfileService } from '../../services/profile'; -import { UIService } from '../../services/ui'; -@Component({ - selector: 'app-private-threads', - templateUrl: './private-threads.component.html', - styleUrls: ['./private-threads.component.scss'] -}) -export class PrivateThreadsComponent { - profile?: NostrProfileDocument; - privateThreads: PrivateThread[] = []; - privateThreadsSubId = '' - privateNotesSubId = '' - - private _editThread: PrivateThread | null = null; - - activeThreadPubKey: string | null = null; - activeThread: PrivateThread | undefined; - isEmojiPickerVisible = false; - @ViewChild('picker') picker: unknown; - @ViewChild('noteContent') noteContent?: FormControl; - noteForm = this.fb.group({ - content: ['', Validators.required], - expiration: [''], - dateControl: [], - }); - - constructor( - private router: Router, - private activatedRoute: ActivatedRoute, - public navigation: NavigationService, - public appState: ApplicationState, - public ui: UIService, - private profileService: ProfileService, - private relayService: RelayService, - public nip76Service: Nip76Service, - private fb: FormBuilder, - ) { } - - async ngOnInit() { - this.activatedRoute.paramMap.subscribe(async (params) => { - this.activeThreadPubKey = params.get('threadPubKey'); - const thread = this.nip76Service.wallet.threads.find(x => this.activeThreadPubKey === x.ap.publicKey.toString('hex')); - if (thread) this.initNotes(thread); - }); - - this.profileService.profile$.subscribe((profile) => { - this.profile = profile; - this.ui.setProfile(this.profile); - }); - - if (this.nip76Service.wallet.isGuest) { - this.randomize(); - } else { - this.nip76Service.privateThreads.subscribe(threads => { - this.initThreads(threads); - }); - } - } - - initThreads(threads: PrivateThread[]) { - if (!this.nip76Service.wallet.isGuest && threads.length) { - - this.privateThreads = threads; - if (this.privateThreadsSubId) { - this.relayService.unsubscribe(this.privateThreadsSubId); - } - - const privateThreads$ = new Subject(); - privateThreads$.subscribe(nostrEvent => { - const thread = this.nip76Service.wallet.threads.find(x => nostrEvent.pubkey === x.ap.publicKey.toString('hex')); - if (thread) { - thread.ownerPubKey = this.profile?.pubkey!; - thread.p = JSON.parse(nostrEvent.content) as IThreadPayload; - thread.ready = true; - } - }); - - const threadPubKeys = this.nip76Service.wallet.threads.map(x => x.ap.publicKey.toString('hex')); - this.privateThreadsSubId = this.relayService.subscribe([{ - authors: threadPubKeys, - kinds: [17761], - limit: 100 - }], 'nip76.privateThreads.List', 'Replaceable', privateThreads$).id; - } - } - - initNotes(thread: PrivateThread | undefined, indexStart = 0) { - this.activeThread = thread; - if (!this.nip76Service.wallet.isGuest && this.activeThread) { - if (this.privateNotesSubId) { - this.relayService.unsubscribe(this.privateNotesSubId); - } - const privateNotes$ = new Subject(); - privateNotes$.subscribe(nostrEvent => { - let keyIndex = 0; - let ap: HDKey; - let sp: HDKey | undefined; - for (let i = indexStart; i < indexStart + 10; i++) { - ap = this.activeThread!.indexMap.post.ap.deriveChildKey(i); - if (nostrEvent.pubkey === ap.publicKey.toString('hex')) { - keyIndex = i; - sp = this.activeThread!.indexMap.post.sp.deriveChildKey(i); - break; - } - } - if (sp) { - const post = PostDocument.default; - post.setKeys(ap!, sp!); - post.address = new HDKissAddress({ publicKey: ap!.publicKey, type: HDKissDocumentType.Post, version: Versions.animiqAPI3 }); - post.thread = this.activeThread!; - post.s = nostrEvent.sig; - post.h = nostrEvent.id; - post.a = post.a = post.address.value; - post.v = 3; - post.e = nostrEvent.content; - post.i = keyIndex; - post.t = nostrEvent.created_at; - // this.activeThread?.indexMap.post.decrypt(post); - const decrypted = post.address.decrypt(post.e, sp.publicKey, post.v); - post.p = JSON.parse(decrypted); - nostrEvent.content = post.p.message!; - post.nostrEvent = nostrEvent; - this.activeThread!.posts.push(post); - } - }); - - const notePubKeys: string[] = []; - for (let i = indexStart; i < indexStart + 10; i++) { - const ap = this.activeThread.indexMap.post.ap.deriveChildKey(i); - notePubKeys.push(ap.publicKey.toString('hex')); - } - this.privateNotesSubId = this.relayService.subscribe([{ - authors: notePubKeys, - kinds: [17761], - limit: 100 - }], `nip76.privateNotes.${this.activeThread.a}.List`, 'Replaceable', privateNotes$).id; - } - } - - public trackByFn(index: number, item: PostDocument) { - return item.a; - } - - randomize() { - this.nip76Service.wallet.reKey(); - this.privateThreads = [this.nip76Service.wallet.threads[0]]; - this._editThread = this.privateThreads[0]; - this.privateThreads[0].pending = 'preview'; - this.privateThreads[0].p.name = 'Example Thread 1'; - this.privateThreads[0].p.description = 'My First Private Trace Resistant Thread 1'; - this.privateThreads[0].ready = true; - } - - async saveKey() { - const savedLocal = await this.nip76Service.saveKeyWithPassword(); - const savedRemote = await this.nip76Service.updateThreadMetadata(this.privateThreads[0]); - // location.reload(); - } - - get editThread(): PrivateThread | null { - return this._editThread!; - } - set editThread(value: PrivateThread | null) { - this.cancelEdit(); - this._editThread = value; - } - - viewThread(thread: PrivateThread) { - this.router.navigate(['profile', 'private-threads', thread.ap.publicKey.toString('hex')]); - } - - addThread() { - this.cancelEdit(); - const firstAvailable = this.privateThreads.find(x => !x.ready); - if (firstAvailable) { - firstAvailable.pending = firstAvailable.a; - firstAvailable.ready = true; - firstAvailable.p.name = 'New Thread'; - this._editThread = firstAvailable; - } - } - - cancelEdit() { - if (this._editThread && this._editThread.pending === this._editThread.a) { - this._editThread.pending = ''; - this._editThread.ready = false; - } - this._editThread = null; - } - - async saveThread() { - this._editThread!.pending = ''; - const savedRemote = await this.nip76Service.updateThreadMetadata(this._editThread!); - if (savedRemote) { - this._editThread = null; - } - } - - public addEmojiNote(event: { emoji: { native: any } }) { - let startPos = (this.noteContent).nativeElement.selectionStart; - let value = this.noteForm.controls.content.value; - - let parsedValue = value?.substring(0, startPos) + event.emoji.native + value?.substring(startPos, value.length); - this.noteForm.controls.content.setValue(parsedValue); - this.isEmojiPickerVisible = false; - - (this.noteContent).nativeElement.focus(); - } - - async saveNote() { - await this.nip76Service.saveNote(this.activeThread!, this.noteForm.controls.content.value!); - } -} From a0cf4bd7448d2e566d3511bd3359636cc285d73c Mon Sep 17 00:00:00 2001 From: d-krause Date: Sat, 11 Mar 2023 03:57:11 -0500 Subject: [PATCH 05/39] adding follows, replies, and reactions, new nprivatethread nip19 --- src/app/app.module.ts | 4 + .../add-thread-dialog.component.html | 2 +- .../nip76-event-buttons.component.ts | 49 ++ .../nip76-event-thread.component.html | 15 + .../nip76-event-thread.component.scss | 13 + .../nip76-event-thread.component.spec.ts | 23 + .../nip76-event-thread.component.ts | 13 + .../nip76-settings.component.html | 68 ++- .../nip76-settings.component.ts | 44 +- src/app/nip76/nip76.service.ts | 450 ++++++++++++------ 10 files changed, 498 insertions(+), 183 deletions(-) create mode 100644 src/app/nip76/nip76-event-buttons/nip76-event-buttons.component.ts create mode 100644 src/app/nip76/nip76-event-thread/nip76-event-thread.component.html create mode 100644 src/app/nip76/nip76-event-thread/nip76-event-thread.component.scss create mode 100644 src/app/nip76/nip76-event-thread/nip76-event-thread.component.spec.ts create mode 100644 src/app/nip76/nip76-event-thread/nip76-event-thread.component.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index dfd9fe6c..1f92c23a 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -154,6 +154,8 @@ import { BadgeComponent } from './badge/badge'; import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; import { Nip76SettingsComponent } from './nip76/nip76-settings/nip76-settings.component'; import { AddThreadDialog } from './nip76/add-thread-dialog/add-thread-dialog.component'; +import { Nip76EventButtonsComponent } from './nip76/nip76-event-buttons/nip76-event-buttons.component'; +import { Nip76EventThreadComponent } from './nip76/nip76-event-thread/nip76-event-thread.component'; @NgModule({ declarations: [ @@ -247,6 +249,8 @@ import { AddThreadDialog } from './nip76/add-thread-dialog/add-thread-dialog.com BadgeComponent, Nip76SettingsComponent, AddThreadDialog, + Nip76EventButtonsComponent, + Nip76EventThreadComponent, ], imports: [ AboutModule, diff --git a/src/app/nip76/add-thread-dialog/add-thread-dialog.component.html b/src/app/nip76/add-thread-dialog/add-thread-dialog.component.html index c508dfab..7358cb8b 100644 --- a/src/app/nip76/add-thread-dialog/add-thread-dialog.component.html +++ b/src/app/nip76/add-thread-dialog/add-thread-dialog.component.html @@ -4,7 +4,7 @@

Thread Preview

key nsecthread - +
diff --git a/src/app/nip76/nip76-event-buttons/nip76-event-buttons.component.ts b/src/app/nip76/nip76-event-buttons/nip76-event-buttons.component.ts new file mode 100644 index 00000000..67759343 --- /dev/null +++ b/src/app/nip76/nip76-event-buttons/nip76-event-buttons.component.ts @@ -0,0 +1,49 @@ +import { Component, ElementRef, HostListener, Input, ViewChild } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Kind } from 'nostr-tools'; +import { DataService } from 'src/app/services/data'; +import { EventService } from 'src/app/services/event'; +import { NostrEventDocument } from 'src/app/services/interfaces'; +import { OptionsService } from 'src/app/services/options'; +import { ProfileService } from 'src/app/services/profile'; +import { Utilities } from 'src/app/services/utilities'; +import { PostDocument } from '../../../../../../animiq-nip76-tools/dist/src'; +import { EventButtonsComponent } from '../../shared/event-buttons/event-buttons'; +import { Nip76Service } from '../nip76.service'; +@Component({ + selector: 'app-nip76-event-buttons', + templateUrl: '../../shared/event-buttons/event-buttons.html', + styleUrls: ['../../shared/event-buttons/event-buttons.css'] +}) +export class Nip76EventButtonsComponent extends EventButtonsComponent { + + private _doc!: PostDocument; + @Input() + set doc(doc: PostDocument) { + this._doc = doc; + this.event = doc.nostrEvent; + } + get doc(): PostDocument { return this._doc; } + + constructor( + private nip76Service: Nip76Service, + eventService: EventService, + dataService: DataService, + optionsService: OptionsService, + profileService: ProfileService, + utilities: Utilities, + dialog: MatDialog) { + super(eventService, dataService, optionsService, profileService, utilities, dialog); + } + + override async addEmoji(e: { emoji: { native: any } }) { + this.isEmojiPickerVisible = false; + const reactionDoc = await this.nip76Service.saveReaction(this.doc, e.emoji.native, 1); + } + + override async addReply() { + this.isEmojiPickerVisible = false; + const reactionDoc = await this.nip76Service.saveReaction(this.doc, this.note, 2); + this.hideReply(); + } +} \ No newline at end of file diff --git a/src/app/nip76/nip76-event-thread/nip76-event-thread.component.html b/src/app/nip76/nip76-event-thread/nip76-event-thread.component.html new file mode 100644 index 00000000..6e30555d --- /dev/null +++ b/src/app/nip76/nip76-event-thread/nip76-event-thread.component.html @@ -0,0 +1,15 @@ +
+ +
+ + + + + +
+ + + + +
+
\ No newline at end of file diff --git a/src/app/nip76/nip76-event-thread/nip76-event-thread.component.scss b/src/app/nip76/nip76-event-thread/nip76-event-thread.component.scss new file mode 100644 index 00000000..cc2fe2d8 --- /dev/null +++ b/src/app/nip76/nip76-event-thread/nip76-event-thread.component.scss @@ -0,0 +1,13 @@ +.thread-event { + margin-left: 27px; + /* border-left: 2px solid rgba(255, 255, 255, 0.15); */ + padding-top: 0em; + padding-left: 1em; +} + +.thread-content { + margin-left: 27px; + padding-left: 1em; + /* border-left: 2px solid rgba(255, 255, 255, 0.15) !important; */ + display: block; +} \ No newline at end of file diff --git a/src/app/nip76/nip76-event-thread/nip76-event-thread.component.spec.ts b/src/app/nip76/nip76-event-thread/nip76-event-thread.component.spec.ts new file mode 100644 index 00000000..2d850a46 --- /dev/null +++ b/src/app/nip76/nip76-event-thread/nip76-event-thread.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Nip76EventThreadComponent } from './nip76-event-thread.component'; + +describe('Nip76EventThreadComponent', () => { + let component: Nip76EventThreadComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ Nip76EventThreadComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(Nip76EventThreadComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/nip76/nip76-event-thread/nip76-event-thread.component.ts b/src/app/nip76/nip76-event-thread/nip76-event-thread.component.ts new file mode 100644 index 00000000..d7d69e71 --- /dev/null +++ b/src/app/nip76/nip76-event-thread/nip76-event-thread.component.ts @@ -0,0 +1,13 @@ +import { Component, Input } from '@angular/core'; +import { PostDocument } from '../../../../../../animiq-nip76-tools/dist/src'; + +@Component({ + selector: 'app-nip76-event-thread', + templateUrl: './nip76-event-thread.component.html', + styleUrls: ['./nip76-event-thread.component.scss'] +}) +export class Nip76EventThreadComponent { + @Input() + doc!: PostDocument; + +} diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.html b/src/app/nip76/nip76-settings/nip76-settings.component.html index 2a474cb4..867e673a 100644 --- a/src/app/nip76/nip76-settings/nip76-settings.component.html +++ b/src/app/nip76/nip76-settings/nip76-settings.component.html @@ -15,10 +15,12 @@

NIP 76 Private Threads are not initialized for this profile yet.

To get started:

    -
  1. Randomize the thread profile - keys. (OPTIONAL).
  2. -
  3. Save it to begin using this - thread profile.
  4. +
  5. + Randomize the keys. (OPTIONAL). +
  6. +
  7. + Save it to begin using. +
@@ -39,9 +41,9 @@
- -
-
+ +
+
@@ -90,19 +92,47 @@
+
+ + + +
+ + +
+
+ + Index + + + + Name + + + + Description + + +
+
+
+
-
+
Private Thread {{activeThread.address.formatted}}
-
-
- Private Thread {{activeThread.address.formatted}}
- +
+
Private Thread {{activeThread.address.formatted}}
+
+ + +
@@ -127,11 +157,15 @@
- -
- - - + + + + +
+ + + + + + +
- - - + + +
@@ -92,31 +100,7 @@
-
- - - -
- - -
-
- - Index - - - - Name - - - - About - - -
-
-
-
+ TBD
@@ -153,10 +137,11 @@ {{ post.nostrEvent.created_at | ago }} + verified_user
- +
{{ item.key }} {{ item.value }}
diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.ts b/src/app/nip76/nip76-settings/nip76-settings.component.ts index 195e8e6f..3c701432 100644 --- a/src/app/nip76/nip76-settings/nip76-settings.component.ts +++ b/src/app/nip76/nip76-settings/nip76-settings.component.ts @@ -34,7 +34,7 @@ export class Nip76SettingsComponent { } get readyPosts(): PostDocument[] { - return this.activeChannel ? this.activeChannel.posts.filter(x => x.ready) : []; + return this.activeChannel ? this.activeChannel.posts : []; } get activeChannel(): PrivateChannel | undefined { diff --git a/src/app/nip76/nip76.service.ts b/src/app/nip76/nip76.service.ts index abcf5ab2..030365d0 100644 --- a/src/app/nip76/nip76.service.ts +++ b/src/app/nip76/nip76.service.ts @@ -138,29 +138,8 @@ export class Nip76Service { fchannel.content = foo.content; fchannel.ready = foo.ready; } - } else if (nostrEvent.tags[0][0] === 'e') { - const post = await channel.hdkIndex.readEvent(nostrEvent) as PostDocument; - if (post) { - post.channel = channel; - nostrEvent.content = post.content?.text! || nostrEvent.content; - channel.posts = [...channel.posts, post].sort((a, b) => b.nostrEvent.created_at - a.nostrEvent.created_at); - var i = channel.posts.length; - while (i--) { - const p1 = channel.posts[i]; - if (p1.content.tags?.length && p1.content.tags[0][0] == 'e') { - const p2 = channel.posts.find(x => x.nostrEvent.id === p1.content.tags![0][1]); - if (p2) { - channel.posts.splice(i, 1); - if (p1.content.kind === nostrTools.Kind.Text) { - p2.replies.push(p1); - } else { - const count = p2.reactionTracker[p1.content.text!]; - p2.reactionTracker[p1.content.text!] = count ? count + 1 : 1; - } - } - } - } - } + } else { + await channel.hdkIndex.readEvent(nostrEvent); } }); channel.channelSubscription = this.relayService.subscribe( @@ -174,11 +153,7 @@ export class Nip76Service { } findChannel(pubkey: string): PrivateChannelWithRelaySub | undefined { - let channel = this.wallet?.channels.find(x => pubkey === x.hdkIndex.signingParent.nostrPubKey); - if (!channel) { - channel = this.wallet?.following.find(x => pubkey === x.hdkIndex.signingParent.nostrPubKey); - } - return channel; + return this.wallet?.channels.find(x => pubkey === x.hdkIndex.signingParent.nostrPubKey); } private async loadFollowing(channelPointer: string | nip19Extension.PrivateChannelPointer, secret?: string | Uint8Array[]) { @@ -190,7 +165,7 @@ export class Nip76Service { pointer = channelPointer; } if (pointer) { - const channel = PrivateChannel.fromPointer(pointer, this.wallet.signingKey) as PrivateChannelWithRelaySub; + const channel = PrivateChannel.fromPointer(pointer) as PrivateChannelWithRelaySub; if (channel.ownerPubKey === this.wallet.ownerPubKey) { const message = 'Self owned channels should only be initialized from the wallet directly.'; this.snackBar.open(message, 'Hide', { @@ -211,7 +186,7 @@ export class Nip76Service { channel.hdkIndex.eventTag = channel.hdkIndex.signingParent.deriveChildKey(0).deriveChildKey(0).pubKeyHash; this.relayService.unsubscribe(channel.channelSubscription!.id); this.loadChannel(channel); - this.wallet.following.push(channel); + this.wallet.channels.push(channel); } }); channel.channelSubscription = this.relayService.subscribe( @@ -247,7 +222,7 @@ export class Nip76Service { ownerPubKey: followDocument.content.owner, signingKey: hexToBytes(followDocument.content.signing_key), cryptoKey: hexToBytes(followDocument.content.crypto_key), - // relays: followDocument.content + relays: followDocument.content.relays } await this.loadFollowing(pointer); } @@ -289,12 +264,12 @@ export class Nip76Service { text, tags: [['e', post.nostrEvent.id]] }; - const event = await post.channel.hdkIndex.createEvent(postDocument, privateKey); + const event = await post.hdkIndex.createEvent(postDocument, privateKey); await this.dataService.publishEvent(event); return postDocument; } - async saveFollowing(following: PrivateChannelWithRelaySub) { + async saveFollowing(following: PrivateChannel) { const privateKey = await this.passwordDialog('Save Follow'); const followDocument = new FollowDocument(); followDocument.content = { @@ -302,7 +277,8 @@ export class Nip76Service { kind: nostrTools.Kind.Contacts, signing_key: bytesToHex(following.hdkIndex.signingParent.publicKey), crypto_key: bytesToHex(following.hdkIndex.cryptoParent.publicKey), - owner: following.ownerPubKey + owner: following.ownerPubKey, + relays: following.content.relays } const event = await this.wallet.documentsIndex.createEvent(followDocument, privateKey); await this.dataService.publishEvent(event); From 63f279e1614814c619ce1b76e70a6b7d64904d7a Mon Sep 17 00:00:00 2001 From: d-krause Date: Mon, 27 Mar 2023 19:46:46 -0400 Subject: [PATCH 16/39] condensing wallet indexes down to one documentsIndex. removing isTopLevel concept,changing ContentDocument serialization. getting ready for invitations and rsvps --- src/app/app.module.ts | 4 +- .../add-thread-dialog.component.html | 2 +- .../nip76-add-invitation.component.html | 16 ++ .../nip76-add-invitation.component.scss | 0 .../nip76-add-invitation.component.spec.ts | 23 +++ .../nip76-add-invitation.component.ts | 21 +++ .../nip76-settings.component.html | 27 ++- .../nip76-settings.component.scss | 5 + .../nip76-settings.component.ts | 10 +- src/app/nip76/nip76.service.ts | 177 ++++++++++-------- 10 files changed, 184 insertions(+), 101 deletions(-) create mode 100644 src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.html create mode 100644 src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.scss create mode 100644 src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.spec.ts create mode 100644 src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index c45fd22d..8358d54c 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -158,6 +158,7 @@ import { AddChannelDialog } from './nip76/add-thread-dialog/add-thread-dialog.co import { Nip76EventButtonsComponent } from './nip76/nip76-event-buttons/nip76-event-buttons.component'; import { Nip76EventThreadComponent } from './nip76/nip76-event-thread/nip76-event-thread.component'; import { Nip76ContentComponent } from './nip76/nip76-content/nip76-event-content.component'; +import { Nip76AddInvitationComponent } from './nip76/nip76-add-invitation/nip76-add-invitation.component'; @NgModule({ declarations: [ @@ -253,7 +254,8 @@ import { Nip76ContentComponent } from './nip76/nip76-content/nip76-event-content AddChannelDialog, Nip76EventButtonsComponent, Nip76EventThreadComponent, - Nip76ContentComponent + Nip76ContentComponent, + Nip76AddInvitationComponent ], imports: [ AboutModule, diff --git a/src/app/nip76/add-thread-dialog/add-thread-dialog.component.html b/src/app/nip76/add-thread-dialog/add-thread-dialog.component.html index bfef6f04..0271fae5 100644 --- a/src/app/nip76/add-thread-dialog/add-thread-dialog.component.html +++ b/src/app/nip76/add-thread-dialog/add-thread-dialog.component.html @@ -3,7 +3,7 @@

Thread Preview

key - nsecthread + nprivatechan
diff --git a/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.html b/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.html new file mode 100644 index 00000000..0271fae5 --- /dev/null +++ b/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.html @@ -0,0 +1,16 @@ +
+

Thread Preview

+
+ + key + nprivatechan + + +
+ +
+ + +
+
+ \ No newline at end of file diff --git a/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.scss b/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.spec.ts b/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.spec.ts new file mode 100644 index 00000000..43aca4c0 --- /dev/null +++ b/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Nip76AddInvitationComponent } from './nip76-add-invitation.component'; + +describe('Nip76AddInvitationComponent', () => { + let component: Nip76AddInvitationComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ Nip76AddInvitationComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(Nip76AddInvitationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.ts b/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.ts new file mode 100644 index 00000000..2d44a14c --- /dev/null +++ b/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.ts @@ -0,0 +1,21 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; + +export interface AddInvitationDialogData { + channelPointer: string +} + +@Component({ + selector: 'app-nip76-add-invitation', + templateUrl: './nip76-add-invitation.component.html', + styleUrls: ['./nip76-add-invitation.component.scss'] +}) +export class Nip76AddInvitationComponent { + constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: AddInvitationDialogData) {} + + onNoClick(): void { + this.data.channelPointer = ''; + this.dialogRef.close(); + } +} + diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.html b/src/app/nip76/nip76-settings/nip76-settings.component.html index ff6490b2..56b9a935 100644 --- a/src/app/nip76/nip76-settings/nip76-settings.component.html +++ b/src/app/nip76/nip76-settings/nip76-settings.component.html @@ -48,6 +48,7 @@
+
{{channel.hdkIndex.signingParent.nostrPubKey}}
@@ -63,24 +64,18 @@
- - Index - - - - Name - - - - About - - +
Name
+
{{channel.content.name}}
+
About
+
{{channel.content.about}} 
+
Invitations + add_circle +
+
+ +
- - Index - - Name diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.scss b/src/app/nip76/nip76-settings/nip76-settings.component.scss index e1103b1d..98e43126 100644 --- a/src/app/nip76/nip76-settings/nip76-settings.component.scss +++ b/src/app/nip76/nip76-settings/nip76-settings.component.scss @@ -46,4 +46,9 @@ li.instruction { .new-post-button:hover { opacity: 0.6; +} + +.field-label { + font-weight: bold; + margin-top: 4px; } \ No newline at end of file diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.ts b/src/app/nip76/nip76-settings/nip76-settings.component.ts index 3c701432..6e36b273 100644 --- a/src/app/nip76/nip76-settings/nip76-settings.component.ts +++ b/src/app/nip76/nip76-settings/nip76-settings.component.ts @@ -146,13 +146,9 @@ export class Nip76SettingsComponent { addChannel() { this.cancelEdit(); - let firstAvailable = this.wallet.channels.find(x => !x.ready); - if (!firstAvailable) { - firstAvailable = this.wallet.getChannel(this.wallet.channels.length); - } - firstAvailable.ready = firstAvailable.editing = true; - firstAvailable.content.name = 'New Channel'; - this._editChannel = firstAvailable; + let newChannel = this.wallet.createChannel() + newChannel.ready = newChannel.editing = true; + this._editChannel = newChannel; } cancelEdit() { diff --git a/src/app/nip76/nip76.service.ts b/src/app/nip76/nip76.service.ts index 030365d0..585c2da5 100644 --- a/src/app/nip76/nip76.service.ts +++ b/src/app/nip76/nip76.service.ts @@ -2,7 +2,7 @@ import { hexToBytes, bytesToHex } from '@noble/hashes/utils'; import { Injectable } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { FollowDocument, nip19Extension, Nip76Wallet, Nip76WebWalletStorage, PostDocument, PrivateChannel } from 'animiq-nip76-tools'; +import { FollowDocument, HDKIndex, HDKIndexType, Keyset, nip19Extension, Nip76Wallet, Nip76WebWalletStorage, PostDocument, PrivateChannel } from 'animiq-nip76-tools'; import * as nostrTools from 'nostr-tools'; import { startWith, Subject } from 'rxjs'; import { DataService } from '../services/data'; @@ -13,6 +13,7 @@ import { SecurityService } from '../services/security'; import { UIService } from '../services/ui'; import { PasswordDialog, PasswordDialogData } from '../shared/password-dialog/password-dialog'; import { AddChannelDialog, AddChannelDialogData } from './add-thread-dialog/add-thread-dialog.component'; +import { AddInvitationDialogData, Nip76AddInvitationComponent } from './nip76-add-invitation/nip76-add-invitation.component'; const nostrPrivKeyAddress = 'blockcore:notes:nostr:prvkey'; @@ -39,9 +40,9 @@ export class Nip76Service { Nip76WebWalletStorage.fromStorage({ publicKey: this.profileService.profile!.pubkey }).then(wallet => { this.wallet = wallet; if (this.wallet.isInSession) { - Array(4).forEach((_, i) => wallet.getChannel(i)); - this.loadChannel(wallet.channels[0]); - this.loadFollowings(); + // Array(4).forEach((_, i) => wallet.getChannel(i)); + // this.loadChannel(wallet.channels[0]); + this.loadDocuments(); } else if (!this.wallet.isGuest) { this.login(); } @@ -86,9 +87,9 @@ export class Nip76Service { const privateKey = await this.passwordDialog('Load Private Channel Keys'); this.wallet = await Nip76WebWalletStorage.fromStorage({ privateKey }); if (this.wallet.isInSession) { - Array(4).forEach((_, i) => this.wallet.getChannel(i)); - this.loadChannel(this.wallet.channels[0]); - this.loadFollowings(); + // Array(4).forEach((_, i) => this.wallet.getChannel(i)); + // this.loadChannel(this.wallet.channels[0]); + this.loadDocuments(); } return this.wallet.isInSession; } @@ -103,7 +104,64 @@ export class Nip76Service { this.wallet = await Nip76WebWalletStorage.fromStorage({ publicKey: this.wallet.ownerPubKey }); } - async previewChannel(): Promise { + documentsSubscription?: NostrRelaySubscription; + + loadDocuments(start = 1, length = 20) { + const authors = this.wallet.documentsIndex.getDocKeys(); + if (this.documentsSubscription) { + this.relayService.unsubscribe(this.documentsSubscription.id); + } + const privateNotes$ = new Subject(); + privateNotes$.subscribe(async nostrEvent => { + const sequentialIndex = authors.findIndex(x => x === nostrEvent.pubkey) + start; + const doc = await this.wallet.documentsIndex.readEvent(nostrEvent, sequentialIndex); + if (doc instanceof PrivateChannel) { //} && doc.hdkIndex === this.wallet.documentsIndex) { + // const keyset = this.wallet.documentsIndex.getKeysFromIndex(sequentialIndex); + // doc.hdkIndex = new HDKIndex(HDKIndexType.TimeBased, keyset.signingKey!, keyset.cryptoKey); + this.loadChannel(doc); + } + // const followDocument = await this.wallet.documentsIndex.readEvent(nostrEvent) as FollowDocument; + // if (followDocument) { + // const pointer: nip19Extension.PrivateChannelPointer = { + // ownerPubKey: followDocument.content.owner, + // signingKey: hexToBytes(followDocument.content.signing_key), + // cryptoKey: hexToBytes(followDocument.content.crypto_key), + // relays: followDocument.content.relays + // } + // await this.loadFollowing(pointer); + // } + }); + + this.documentsSubscription = this.relayService.subscribe([{ + authors, + kinds: [17761], + limit: length + }], `nip76Service.loadDocuments.${start}-${length}`, 'Replaceable', privateNotes$); + } + + loadChannel(channel: PrivateChannelWithRelaySub, startIndex = 0, length = 20) { + if (channel.channelSubscription) { + this.relayService.unsubscribe(channel.channelSubscription.id); + } + const privateChannel$ = new Subject(); + privateChannel$.subscribe(async nostrEvent => { + await channel.hdkIndex.readEvent(nostrEvent); + }); + channel.channelSubscription = this.relayService.subscribe( + [{ + '#e': [channel.hdkIndex.eventTag], + kinds: [17761], + limit: length + }], + `nip76Service.loadChannel.${channel.hdkIndex.eventTag}`, 'Replaceable', privateChannel$ + ); + } + + findChannel(pubkey: string): PrivateChannelWithRelaySub | undefined { + return this.wallet?.channels.find(x => pubkey === x.hdkIndex.signingParent.nostrPubKey); + } + + async previewChannel(): Promise { return new Promise((resolve, reject) => { const dialogRef = this.dialog.open(AddChannelDialog, { data: { channelPointer: '' }, @@ -125,36 +183,28 @@ export class Nip76Service { }); } - loadChannel(channel: PrivateChannelWithRelaySub, startIndex = 0, length = 20) { - if (channel.channelSubscription) { - this.relayService.unsubscribe(channel.channelSubscription.id); - } - const privateChannel$ = new Subject(); - privateChannel$.subscribe(async nostrEvent => { - let fchannel = this.findChannel(nostrEvent.pubkey); - if (fchannel) { - const foo = await fchannel.hdkIndex.readEvent(nostrEvent, true) as PrivateChannel; - if (foo) { - fchannel.content = foo.content; - fchannel.ready = foo.ready; + async addInvitation(): Promise { + return new Promise((resolve, reject) => { + const dialogRef = this.dialog.open(Nip76AddInvitationComponent, { + data: { channelPointer: '' }, + maxWidth: '200vw', + panelClass: 'full-width-dialog', + }); + dialogRef.afterClosed().subscribe(async (result: AddInvitationDialogData) => { + if (result?.channelPointer) { + const following = await this.loadFollowing(result.channelPointer, ''); + if (following) { + resolve(following); + } else { + reject(); + } + } else { + reject(); } - } else { - await channel.hdkIndex.readEvent(nostrEvent); - } + }); }); - channel.channelSubscription = this.relayService.subscribe( - [{ - '#e': [channel.hdkIndex.eventTag], - kinds: [17761], - limit: length - }], - `nip76Service.loadChannel.${channel.hdkIndex.eventTag}`, 'Replaceable', privateChannel$ - ); } - findChannel(pubkey: string): PrivateChannelWithRelaySub | undefined { - return this.wallet?.channels.find(x => pubkey === x.hdkIndex.signingParent.nostrPubKey); - } private async loadFollowing(channelPointer: string | nip19Extension.PrivateChannelPointer, secret?: string | Uint8Array[]) { let pointer: nip19Extension.PrivateChannelPointer; @@ -177,17 +227,17 @@ export class Nip76Service { } const privateChannel$ = new Subject(); privateChannel$.subscribe(async nostrEvent => { - const fchannel = await channel.hdkIndex.readEvent(nostrEvent, true) as PrivateChannel; - if (fchannel) { - channel.content = fchannel.content; - channel.ready = fchannel.ready; - channel.hdkIndex.signingParent._chainCode = hexToBytes(fchannel.content.chain_sign!); - channel.hdkIndex.cryptoParent._chainCode = hexToBytes(fchannel.content.chain_crypto!); - channel.hdkIndex.eventTag = channel.hdkIndex.signingParent.deriveChildKey(0).deriveChildKey(0).pubKeyHash; - this.relayService.unsubscribe(channel.channelSubscription!.id); - this.loadChannel(channel); - this.wallet.channels.push(channel); - } + // const fchannel = await channel.hdkIndex.readEvent(nostrEvent, channel) as PrivateChannel; + // if (fchannel) { + // channel.content = fchannel.content; + // channel.ready = fchannel.ready; + // channel.hdkIndex.signingParent._chainCode = hexToBytes(fchannel.content.chain_sign!); + // channel.hdkIndex.cryptoParent._chainCode = hexToBytes(fchannel.content.chain_crypto!); + // channel.hdkIndex.eventTag = channel.hdkIndex.signingParent.deriveChildKey(0).deriveChildKey(0).pubKeyHash; + // this.relayService.unsubscribe(channel.channelSubscription!.id); + // this.loadChannel(channel); + // this.wallet.channels.push(channel); + // } }); channel.channelSubscription = this.relayService.subscribe( [{ @@ -208,41 +258,16 @@ export class Nip76Service { } } - followingSubscription?: NostrRelaySubscription; - - loadFollowings() { - if (this.followingSubscription) { - this.relayService.unsubscribe(this.followingSubscription.id); - } - const privateNotes$ = new Subject(); - privateNotes$.subscribe(async nostrEvent => { - const followDocument = await this.wallet.documentsIndex.readEvent(nostrEvent) as FollowDocument; - if (followDocument) { - const pointer: nip19Extension.PrivateChannelPointer = { - ownerPubKey: followDocument.content.owner, - signingKey: hexToBytes(followDocument.content.signing_key), - cryptoKey: hexToBytes(followDocument.content.crypto_key), - relays: followDocument.content.relays - } - await this.loadFollowing(pointer); - } - }); - - this.followingSubscription = this.relayService.subscribe([{ - '#e': [this.wallet.documentsIndex.eventTag], - kinds: [17761], - limit: 100 - }], `nip76Service.loadFollowings.${startWith}`, 'Replaceable', privateNotes$); - } - - async saveChannel(channel: PrivateChannelWithRelaySub, privateKey?: string) { + async saveChannel(channel: PrivateChannel, privateKey?: string) { privateKey = privateKey || await this.passwordDialog('Save Channel Details'); - const ev = await channel.hdkIndex.createEvent(channel, privateKey); - await this.dataService.publishEvent(ev); + if (!channel.nostrEvent) { + channel.nostrEvent = await this.wallet.documentsIndex.createEvent(channel, privateKey); + } + await this.dataService.publishEvent(channel.nostrEvent); return true; } - async saveNote(channel: PrivateChannelWithRelaySub, text: string) { + async saveNote(channel: PrivateChannel, text: string) { const privateKey = await this.passwordDialog('Save Note'); const postDocument = new PostDocument(); postDocument.content = { From 880e4309f83d1935f3e7e0abdafebc254610e052 Mon Sep 17 00:00:00 2001 From: d-krause Date: Mon, 27 Mar 2023 19:52:02 -0400 Subject: [PATCH 17/39] renming add-thread to add-channel --- src/app/app.module.ts | 2 +- .../add-thread-dialog.component.html | 0 .../add-thread-dialog.component.scss | 0 .../add-thread-dialog.component.spec.ts | 0 .../add-thread-dialog.component.ts | 0 src/app/nip76/nip76.service.ts | 2 +- 6 files changed, 2 insertions(+), 2 deletions(-) rename src/app/nip76/{add-thread-dialog => nip76-add-channel-dialog}/add-thread-dialog.component.html (100%) rename src/app/nip76/{add-thread-dialog => nip76-add-channel-dialog}/add-thread-dialog.component.scss (100%) rename src/app/nip76/{add-thread-dialog => nip76-add-channel-dialog}/add-thread-dialog.component.spec.ts (100%) rename src/app/nip76/{add-thread-dialog => nip76-add-channel-dialog}/add-thread-dialog.component.ts (100%) diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 8358d54c..81c078f0 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -154,7 +154,7 @@ import { BadgeComponent } from './badge/badge'; import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; import { DragScrollModule } from 'ngx-drag-scroll'; import { Nip76SettingsComponent } from './nip76/nip76-settings/nip76-settings.component'; -import { AddChannelDialog } from './nip76/add-thread-dialog/add-thread-dialog.component'; +import { AddChannelDialog } from './nip76/nip76-add-channel-dialog/add-thread-dialog.component'; import { Nip76EventButtonsComponent } from './nip76/nip76-event-buttons/nip76-event-buttons.component'; import { Nip76EventThreadComponent } from './nip76/nip76-event-thread/nip76-event-thread.component'; import { Nip76ContentComponent } from './nip76/nip76-content/nip76-event-content.component'; diff --git a/src/app/nip76/add-thread-dialog/add-thread-dialog.component.html b/src/app/nip76/nip76-add-channel-dialog/add-thread-dialog.component.html similarity index 100% rename from src/app/nip76/add-thread-dialog/add-thread-dialog.component.html rename to src/app/nip76/nip76-add-channel-dialog/add-thread-dialog.component.html diff --git a/src/app/nip76/add-thread-dialog/add-thread-dialog.component.scss b/src/app/nip76/nip76-add-channel-dialog/add-thread-dialog.component.scss similarity index 100% rename from src/app/nip76/add-thread-dialog/add-thread-dialog.component.scss rename to src/app/nip76/nip76-add-channel-dialog/add-thread-dialog.component.scss diff --git a/src/app/nip76/add-thread-dialog/add-thread-dialog.component.spec.ts b/src/app/nip76/nip76-add-channel-dialog/add-thread-dialog.component.spec.ts similarity index 100% rename from src/app/nip76/add-thread-dialog/add-thread-dialog.component.spec.ts rename to src/app/nip76/nip76-add-channel-dialog/add-thread-dialog.component.spec.ts diff --git a/src/app/nip76/add-thread-dialog/add-thread-dialog.component.ts b/src/app/nip76/nip76-add-channel-dialog/add-thread-dialog.component.ts similarity index 100% rename from src/app/nip76/add-thread-dialog/add-thread-dialog.component.ts rename to src/app/nip76/nip76-add-channel-dialog/add-thread-dialog.component.ts diff --git a/src/app/nip76/nip76.service.ts b/src/app/nip76/nip76.service.ts index 585c2da5..8884a835 100644 --- a/src/app/nip76/nip76.service.ts +++ b/src/app/nip76/nip76.service.ts @@ -12,7 +12,7 @@ import { RelayService } from '../services/relay'; import { SecurityService } from '../services/security'; import { UIService } from '../services/ui'; import { PasswordDialog, PasswordDialogData } from '../shared/password-dialog/password-dialog'; -import { AddChannelDialog, AddChannelDialogData } from './add-thread-dialog/add-thread-dialog.component'; +import { AddChannelDialog, AddChannelDialogData } from './nip76-add-channel-dialog/add-thread-dialog.component'; import { AddInvitationDialogData, Nip76AddInvitationComponent } from './nip76-add-invitation/nip76-add-invitation.component'; const nostrPrivKeyAddress = 'blockcore:notes:nostr:prvkey'; From bb4d9501aea5ce91d711fcb4636da5297a6987b7 Mon Sep 17 00:00:00 2001 From: d-krause Date: Mon, 27 Mar 2023 19:54:35 -0400 Subject: [PATCH 18/39] renaming add-thread to add-channel --- src/app/app.module.ts | 2 +- ...log.component.html => add-channel-dialog.component.html} | 0 ...log.component.scss => add-channel-dialog.component.scss} | 0 ...mponent.spec.ts => add-channel-dialog.component.spec.ts} | 2 +- ...-dialog.component.ts => add-channel-dialog.component.ts} | 6 +++--- src/app/nip76/nip76.service.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) rename src/app/nip76/nip76-add-channel-dialog/{add-thread-dialog.component.html => add-channel-dialog.component.html} (100%) rename src/app/nip76/nip76-add-channel-dialog/{add-thread-dialog.component.scss => add-channel-dialog.component.scss} (100%) rename src/app/nip76/nip76-add-channel-dialog/{add-thread-dialog.component.spec.ts => add-channel-dialog.component.spec.ts} (89%) rename src/app/nip76/nip76-add-channel-dialog/{add-thread-dialog.component.ts => add-channel-dialog.component.ts} (75%) diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 81c078f0..e082690f 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -154,7 +154,7 @@ import { BadgeComponent } from './badge/badge'; import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; import { DragScrollModule } from 'ngx-drag-scroll'; import { Nip76SettingsComponent } from './nip76/nip76-settings/nip76-settings.component'; -import { AddChannelDialog } from './nip76/nip76-add-channel-dialog/add-thread-dialog.component'; +import { AddChannelDialog } from './nip76/nip76-add-channel-dialog/add-channel-dialog.component'; import { Nip76EventButtonsComponent } from './nip76/nip76-event-buttons/nip76-event-buttons.component'; import { Nip76EventThreadComponent } from './nip76/nip76-event-thread/nip76-event-thread.component'; import { Nip76ContentComponent } from './nip76/nip76-content/nip76-event-content.component'; diff --git a/src/app/nip76/nip76-add-channel-dialog/add-thread-dialog.component.html b/src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.html similarity index 100% rename from src/app/nip76/nip76-add-channel-dialog/add-thread-dialog.component.html rename to src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.html diff --git a/src/app/nip76/nip76-add-channel-dialog/add-thread-dialog.component.scss b/src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.scss similarity index 100% rename from src/app/nip76/nip76-add-channel-dialog/add-thread-dialog.component.scss rename to src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.scss diff --git a/src/app/nip76/nip76-add-channel-dialog/add-thread-dialog.component.spec.ts b/src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.spec.ts similarity index 89% rename from src/app/nip76/nip76-add-channel-dialog/add-thread-dialog.component.spec.ts rename to src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.spec.ts index 2a995b8a..fb2bd440 100644 --- a/src/app/nip76/nip76-add-channel-dialog/add-thread-dialog.component.spec.ts +++ b/src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { AddChannelDialog } from './add-thread-dialog.component'; +import { AddChannelDialog } from './add-channel-dialog.component'; describe('AddChannelDialogComponent', () => { let component: AddChannelDialog; diff --git a/src/app/nip76/nip76-add-channel-dialog/add-thread-dialog.component.ts b/src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.ts similarity index 75% rename from src/app/nip76/nip76-add-channel-dialog/add-thread-dialog.component.ts rename to src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.ts index 296426f5..f350b423 100644 --- a/src/app/nip76/nip76-add-channel-dialog/add-thread-dialog.component.ts +++ b/src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.ts @@ -6,9 +6,9 @@ export interface AddChannelDialogData { } @Component({ - selector: 'app-add-channel-dialog', - templateUrl: './add-thread-dialog.component.html', - styleUrls: ['./add-thread-dialog.component.scss'] + selector: 'nip76-add-channel-dialog', + templateUrl: './add-channel-dialog.component.html', + styleUrls: ['./add-channel-dialog.component.scss'] }) export class AddChannelDialog { constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: AddChannelDialogData) {} diff --git a/src/app/nip76/nip76.service.ts b/src/app/nip76/nip76.service.ts index 8884a835..6d8fff39 100644 --- a/src/app/nip76/nip76.service.ts +++ b/src/app/nip76/nip76.service.ts @@ -12,7 +12,7 @@ import { RelayService } from '../services/relay'; import { SecurityService } from '../services/security'; import { UIService } from '../services/ui'; import { PasswordDialog, PasswordDialogData } from '../shared/password-dialog/password-dialog'; -import { AddChannelDialog, AddChannelDialogData } from './nip76-add-channel-dialog/add-thread-dialog.component'; +import { AddChannelDialog, AddChannelDialogData } from './nip76-add-channel-dialog/add-channel-dialog.component'; import { AddInvitationDialogData, Nip76AddInvitationComponent } from './nip76-add-invitation/nip76-add-invitation.component'; const nostrPrivKeyAddress = 'blockcore:notes:nostr:prvkey'; From 6ed051ac99e9235a9ab1d14b31c2092234815817 Mon Sep 17 00:00:00 2001 From: d-krause Date: Tue, 28 Mar 2023 15:35:02 -0400 Subject: [PATCH 19/39] adding channel invitations is working, now need to work on reading and storing invitations --- .../add-channel-dialog.component.html | 2 +- .../nip76-add-invitation.component.html | 49 ++++++-- .../nip76-add-invitation.component.ts | 58 +++++++++- .../nip76-settings.component.html | 26 +++-- .../nip76-settings.component.scss | 6 + .../nip76-settings.component.ts | 14 +-- src/app/nip76/nip76.service.ts | 106 ++++++++++++------ 7 files changed, 194 insertions(+), 67 deletions(-) diff --git a/src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.html b/src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.html index 0271fae5..347f2860 100644 --- a/src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.html +++ b/src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.html @@ -1,5 +1,5 @@
-

Thread Preview

+

Channel Preview

key diff --git a/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.html b/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.html index 0271fae5..c52fb4da 100644 --- a/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.html +++ b/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.html @@ -1,16 +1,43 @@
-

Thread Preview

+

Save CHannel Invitation

- - key - nprivatechan - - + + + + By Public Key + By Password + + +
+ + person_add + Public Key + + + +
+
+ + password + Password + + + + password + Confirm Password + + +
+ +

Error: {{ error }}

- +
- - + +
-
- \ No newline at end of file +
\ No newline at end of file diff --git a/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.ts b/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.ts index 2d44a14c..3f28b578 100644 --- a/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.ts +++ b/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.ts @@ -1,8 +1,14 @@ import { Component, Inject } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { nip19 } from 'nostr-tools'; export interface AddInvitationDialogData { - channelPointer: string + invitationType: 'pubkey' | 'password'; + pubkey?: string; + validPubkey?: string; + password?: string; + password2?: string; + } @Component({ @@ -11,11 +17,55 @@ export interface AddInvitationDialogData { styleUrls: ['./nip76-add-invitation.component.scss'] }) export class Nip76AddInvitationComponent { - constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: AddInvitationDialogData) {} + error!: string; + valid = false; + constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: AddInvitationDialogData) { + data.invitationType = 'pubkey'; + } onNoClick(): void { - this.data.channelPointer = ''; this.dialogRef.close(); } + + updatePubkey() { + this.error = ''; + this.data.validPubkey = ''; + try { + if (this.data.pubkey!.startsWith('npub')) { + this.data.pubkey = this.data.validPubkey = nip19.decode(this.data.pubkey!).data as any; + this.valid = true; + } else if (this.data.pubkey!.match(/^[0-9a-f]{64}$/i)) { + this.data.validPubkey = nip19.decode(nip19.npubEncode(this.data.pubkey!)).data as any; + this.valid = true; + } else { + this.error = 'This does not appear to be a valid public key.' + this.valid = false; + } + } catch (err: any) { + this.error = err.message; + this.valid = false; + } + } + + updatePassword() { + this.error = ''; + if (this.data.password!.length < 4) { + this.error = 'Password must be at least 4 characters.' + this.valid = false; + } else if (this.data.password !== this.data.password2) { + this.error = 'Password was not entered the same.' + this.valid = false; + } else { + this.valid = true; + } + } + + clearForm() { + this.error = ''; + this.valid = false; + this.data = { + invitationType: this.data.invitationType + }; + } } diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.html b/src/app/nip76/nip76-settings/nip76-settings.component.html index 56b9a935..6916a239 100644 --- a/src/app/nip76/nip76-settings/nip76-settings.component.html +++ b/src/app/nip76/nip76-settings/nip76-settings.component.html @@ -26,7 +26,7 @@
Index - + Name @@ -48,7 +48,7 @@
-
{{channel.hdkIndex.signingParent.nostrPubKey}}
+
{{channel.dkxPost.signingParent.nostrPubKey}}
@@ -60,7 +60,7 @@ - +
@@ -68,8 +68,18 @@
{{channel.content.name}}
About
{{channel.content.about}} 
-
Invitations - add_circle +
+
Invitations + add_circle +
+
@@ -100,16 +110,16 @@
- Private Thread {{activeChannel.hdkIndex.signingParent.nostrPubKey}}
+ Private Thread {{activeChannel.dkxPost.signingParent.nostrPubKey}}
-
Private Thread {{activeChannel.hdkIndex.signingParent.nostrPubKey}}
+
Private Thread {{activeChannel.dkxPost.signingParent.nostrPubKey}}
- +
diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.scss b/src/app/nip76/nip76-settings/nip76-settings.component.scss index 98e43126..df410793 100644 --- a/src/app/nip76/nip76-settings/nip76-settings.component.scss +++ b/src/app/nip76/nip76-settings/nip76-settings.component.scss @@ -51,4 +51,10 @@ li.instruction { .field-label { font-weight: bold; margin-top: 4px; +} + +.label-only-anchor { + position: relative; + left: 66px; + top: 22px; } \ No newline at end of file diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.ts b/src/app/nip76/nip76-settings/nip76-settings.component.ts index 6e36b273..1a5c8113 100644 --- a/src/app/nip76/nip76-settings/nip76-settings.component.ts +++ b/src/app/nip76/nip76-settings/nip76-settings.component.ts @@ -3,7 +3,7 @@ import { FormBuilder, FormControl, Validators } from '@angular/forms'; import { MatSnackBar } from '@angular/material/snack-bar'; import { MatTabChangeEvent } from '@angular/material/tabs'; import { ActivatedRoute, Router } from '@angular/router'; -import { Nip76Wallet, PostDocument, PrivateChannel } from 'animiq-nip76-tools'; +import { Invitation, Nip76Wallet, PostDocument, PrivateChannel } from 'animiq-nip76-tools'; import { ApplicationState } from '../../services/applicationstate'; import { NavigationService } from '../../services/navigation'; import { UIService } from '../../services/ui'; @@ -108,16 +108,16 @@ export class Nip76SettingsComponent { } viewChannelNotes(channel: PrivateChannel) { - this.router.navigate(['private-channels', channel.hdkIndex.signingParent.nostrPubKey, 'notes']); + this.router.navigate(['private-channels', channel.dkxPost.signingParent.nostrPubKey, 'notes']); } viewChannelFollowers(channel: PrivateChannel) { - this.router.navigate(['private-channels', channel.hdkIndex.signingParent.nostrPubKey, 'followers']); + this.router.navigate(['private-channels', channel.dkxPost.signingParent.nostrPubKey, 'followers']); } - async copyKeys(channel: PrivateChannel) { - navigator.clipboard.writeText(await channel.getChannelPointer()); - this.snackBar.open(`Channel keys are now in your clipboard.`, 'Hide', { + async copyKeys(invite: Invitation) { + navigator.clipboard.writeText(await invite.getPointer()); + this.snackBar.open(`The invitation is now in your clipboard.`, 'Hide', { duration: 3000, horizontalPosition: 'center', verticalPosition: 'bottom', @@ -127,7 +127,7 @@ export class Nip76SettingsComponent { async previewChannel() { const channel = await this.nip76Service.previewChannel(); if (channel) { - this.activeChannelId = channel.hdkIndex.signingParent.nostrPubKey; + this.activeChannelId = channel.dkxPost.signingParent.nostrPubKey; if (this.tabIndex != 1) { // this.viewChannelFollowers(channel); this.router.navigate(['private-channels','following']); diff --git a/src/app/nip76/nip76.service.ts b/src/app/nip76/nip76.service.ts index 6d8fff39..f1dad99b 100644 --- a/src/app/nip76/nip76.service.ts +++ b/src/app/nip76/nip76.service.ts @@ -1,10 +1,10 @@ -import { hexToBytes, bytesToHex } from '@noble/hashes/utils'; import { Injectable } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { FollowDocument, HDKIndex, HDKIndexType, Keyset, nip19Extension, Nip76Wallet, Nip76WebWalletStorage, PostDocument, PrivateChannel } from 'animiq-nip76-tools'; +import { bech32 } from '@scure/base'; +import { FollowDocument, Invitation, nip19Extension, Nip76Wallet, Nip76WebWalletStorage, PostDocument, PrivateChannel } from 'animiq-nip76-tools'; import * as nostrTools from 'nostr-tools'; -import { startWith, Subject } from 'rxjs'; +import { Subject } from 'rxjs'; import { DataService } from '../services/data'; import { NostrEvent, NostrRelaySubscription } from '../services/interfaces'; import { ProfileService } from '../services/profile'; @@ -27,6 +27,7 @@ interface PrivateChannelWithRelaySub extends PrivateChannel { export class Nip76Service { wallet!: Nip76Wallet; + documentsSubscription?: NostrRelaySubscription; constructor( private dialog: MatDialog, @@ -104,17 +105,15 @@ export class Nip76Service { this.wallet = await Nip76WebWalletStorage.fromStorage({ publicKey: this.wallet.ownerPubKey }); } - documentsSubscription?: NostrRelaySubscription; - loadDocuments(start = 1, length = 20) { - const authors = this.wallet.documentsIndex.getDocKeys(); + const authors = this.wallet.documentsIndex.getDocKeys(start, length); if (this.documentsSubscription) { this.relayService.unsubscribe(this.documentsSubscription.id); } const privateNotes$ = new Subject(); privateNotes$.subscribe(async nostrEvent => { - const sequentialIndex = authors.findIndex(x => x === nostrEvent.pubkey) + start; - const doc = await this.wallet.documentsIndex.readEvent(nostrEvent, sequentialIndex); + const docIndex = authors.findIndex(x => x === nostrEvent.pubkey) + start; + const doc = await this.wallet.documentsIndex.readEvent(nostrEvent, docIndex); if (doc instanceof PrivateChannel) { //} && doc.hdkIndex === this.wallet.documentsIndex) { // const keyset = this.wallet.documentsIndex.getKeysFromIndex(sequentialIndex); // doc.hdkIndex = new HDKIndex(HDKIndexType.TimeBased, keyset.signingKey!, keyset.cryptoKey); @@ -139,26 +138,37 @@ export class Nip76Service { }], `nip76Service.loadDocuments.${start}-${length}`, 'Replaceable', privateNotes$); } - loadChannel(channel: PrivateChannelWithRelaySub, startIndex = 0, length = 20) { + loadChannel(channel: PrivateChannelWithRelaySub, start = 1, length = 20) { + const invitePubs = channel.dkxInvite.getDocKeys(start, length); if (channel.channelSubscription) { this.relayService.unsubscribe(channel.channelSubscription.id); } const privateChannel$ = new Subject(); privateChannel$.subscribe(async nostrEvent => { - await channel.hdkIndex.readEvent(nostrEvent); + if (channel.dkxPost.eventTag === nostrEvent.tags[0][1]) { + await channel.dkxPost.readEvent(nostrEvent); + } else { + const docIndex = invitePubs.findIndex(x => x === nostrEvent.pubkey) + start; + await channel.dkxInvite.readEvent(nostrEvent, docIndex); + } }); channel.channelSubscription = this.relayService.subscribe( [{ - '#e': [channel.hdkIndex.eventTag], + '#e': [channel.dkxPost.eventTag], + kinds: [17761], + limit: length + }, + { + authors: invitePubs, kinds: [17761], limit: length }], - `nip76Service.loadChannel.${channel.hdkIndex.eventTag}`, 'Replaceable', privateChannel$ + `nip76Service.loadChannel.${channel.dkxPost.eventTag}`, 'Replaceable', privateChannel$ ); } findChannel(pubkey: string): PrivateChannelWithRelaySub | undefined { - return this.wallet?.channels.find(x => pubkey === x.hdkIndex.signingParent.nostrPubKey); + return this.wallet?.channels.find(x => pubkey === x.dkxPost.signingParent.nostrPubKey); } async previewChannel(): Promise { @@ -170,7 +180,7 @@ export class Nip76Service { }); dialogRef.afterClosed().subscribe(async (result: AddChannelDialogData) => { if (result?.channelPointer) { - const following = await this.loadFollowing(result.channelPointer, ''); + const following = await this.readInvitation(result.channelPointer, ''); if (following) { resolve(following); } else { @@ -183,7 +193,7 @@ export class Nip76Service { }); } - async addInvitation(): Promise { + async addInvitation(channel: PrivateChannel): Promise { return new Promise((resolve, reject) => { const dialogRef = this.dialog.open(Nip76AddInvitationComponent, { data: { channelPointer: '' }, @@ -191,10 +201,10 @@ export class Nip76Service { panelClass: 'full-width-dialog', }); dialogRef.afterClosed().subscribe(async (result: AddInvitationDialogData) => { - if (result?.channelPointer) { - const following = await this.loadFollowing(result.channelPointer, ''); - if (following) { - resolve(following); + if (result) { + const invitation = await this.saveInvitation(channel, result); + if (invitation) { + resolve(invitation); } else { reject(); } @@ -206,9 +216,16 @@ export class Nip76Service { } - private async loadFollowing(channelPointer: string | nip19Extension.PrivateChannelPointer, secret?: string | Uint8Array[]) { + private async readInvitation(channelPointer: string | nip19Extension.PrivateChannelPointer, secret?: string | Uint8Array[]) { let pointer: nip19Extension.PrivateChannelPointer; if (typeof channelPointer === 'string') { + const words = bech32.decode(channelPointer, 5000).words; + const pointerType = Uint8Array.from(bech32.fromWords(words))[0] as nip19Extension.PointerType; + if((pointerType & nip19Extension.PointerType.SharedSecret) == nip19Extension.PointerType.SharedSecret) + { + + debugger + } const p = await nip19Extension.decode(channelPointer, secret!); pointer = p.data as nip19Extension.PrivateChannelPointer; } else { @@ -241,11 +258,11 @@ export class Nip76Service { }); channel.channelSubscription = this.relayService.subscribe( [{ - authors: [channel.hdkIndex.signingParent.nostrPubKey], + authors: [channel.dkxPost.signingParent.nostrPubKey], kinds: [17761], limit: length }], - `nip76Service.loadFollowing.${channel.hdkIndex.eventTag}`, 'Replaceable', privateChannel$ + `nip76Service.loadFollowing.${channel.dkxPost.eventTag}`, 'Replaceable', privateChannel$ ); return channel; } else { @@ -260,10 +277,8 @@ export class Nip76Service { async saveChannel(channel: PrivateChannel, privateKey?: string) { privateKey = privateKey || await this.passwordDialog('Save Channel Details'); - if (!channel.nostrEvent) { - channel.nostrEvent = await this.wallet.documentsIndex.createEvent(channel, privateKey); - } - await this.dataService.publishEvent(channel.nostrEvent); + const ev = await this.wallet.documentsIndex.createEvent(channel, privateKey); + await this.dataService.publishEvent(ev); return true; } @@ -275,7 +290,7 @@ export class Nip76Service { pubkey: this.wallet.ownerPubKey, kind: nostrTools.Kind.Text } - const event = await channel.hdkIndex.createEvent(postDocument, privateKey); + const event = await channel.dkxPost.createEvent(postDocument, privateKey); await this.dataService.publishEvent(event); return true; } @@ -289,22 +304,41 @@ export class Nip76Service { text, tags: [['e', post.nostrEvent.id]] }; - const event = await post.hdkIndex.createEvent(postDocument, privateKey); + const event = await post.dkxParent.createEvent(postDocument, privateKey); await this.dataService.publishEvent(event); return postDocument; } + async saveInvitation(channel: PrivateChannel, invitation: AddInvitationDialogData): Promise { + const privateKey = await this.passwordDialog('Save Invitation'); + const invite = new Invitation(); + invite.content = { + kind: 1776, + for: invitation.invitationType === 'pubkey' ? invitation.validPubkey : undefined, + password: invitation.invitationType === 'password' ? invitation.password : undefined, + pubkey: channel.dkxPost.signingParent.nostrPubKey, + signingParent: channel.dkxPost.signingParent, + cryptoParent: channel.dkxPost.cryptoParent, + } + invite.docIndex = channel.dkxInvite.documents.length + 1; + const event = await channel.dkxInvite.createEvent(invite, privateKey); + await this.dataService.publishEvent(event); + console.log(event); + return invite; + + } + async saveFollowing(following: PrivateChannel) { const privateKey = await this.passwordDialog('Save Follow'); const followDocument = new FollowDocument(); - followDocument.content = { - pubkey: this.wallet.ownerPubKey, - kind: nostrTools.Kind.Contacts, - signing_key: bytesToHex(following.hdkIndex.signingParent.publicKey), - crypto_key: bytesToHex(following.hdkIndex.cryptoParent.publicKey), - owner: following.ownerPubKey, - relays: following.content.relays - } + // followDocument.content = { + // pubkey: this.wallet.ownerPubKey, + // kind: nostrTools.Kind.Contacts, + // signing_key: bytesToHex(following.hdkIndex.signingParent.publicKey), + // crypto_key: bytesToHex(following.hdkIndex.cryptoParent.publicKey), + // owner: following.ownerPubKey, + // relays: following.content.relays + // } const event = await this.wallet.documentsIndex.createEvent(followDocument, privateKey); await this.dataService.publishEvent(event); return true; From 2456d6b06ed2cf5c9887ddc5629efa01a3e41249 Mon Sep 17 00:00:00 2001 From: d-krause Date: Wed, 29 Mar 2023 23:40:02 -0400 Subject: [PATCH 20/39] fully functioning invitations and rsvps --- .../add-channel-dialog.component.html | 33 ++- .../add-channel-dialog.component.ts | 18 +- .../nip76-add-invitation.component.html | 2 +- .../nip76-settings.component.html | 10 +- .../nip76-settings.component.ts | 20 +- src/app/nip76/nip76.service.ts | 249 ++++++++++-------- 6 files changed, 194 insertions(+), 138 deletions(-) diff --git a/src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.html b/src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.html index 347f2860..7b111d5b 100644 --- a/src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.html +++ b/src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.html @@ -1,16 +1,21 @@
-

Channel Preview

-
- - key - nprivatechan - - -
- -
- - -
+

Channel Preview

+
+ + key + nprivatechan + + + + password + Password + +
- \ No newline at end of file + +
+ + +
+
\ No newline at end of file diff --git a/src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.ts b/src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.ts index f350b423..a2dc372d 100644 --- a/src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.ts +++ b/src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.ts @@ -1,8 +1,11 @@ import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { bech32 } from '@scure/base'; +import { nip19Extension } from 'animiq-nip76-tools'; export interface AddChannelDialogData { - channelPointer: string + channelPointer: string; + password?: string; } @Component({ @@ -11,10 +14,23 @@ export interface AddChannelDialogData { styleUrls: ['./add-channel-dialog.component.scss'] }) export class AddChannelDialog { + + requirePassword = false; + constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: AddChannelDialogData) {} onNoClick(): void { this.data.channelPointer = ''; this.dialogRef.close(); } + + onChannelPointerChange(){ + try{ + const words = bech32.decode(this.data.channelPointer, 5000).words; + const pointerType = Uint8Array.from(bech32.fromWords(words))[0] as nip19Extension.PointerType; + this.requirePassword = (pointerType & nip19Extension.PointerType.Password) == nip19Extension.PointerType.Password; + } catch (error) { + this.requirePassword = false; + } + } } diff --git a/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.html b/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.html index c52fb4da..aa956287 100644 --- a/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.html +++ b/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.html @@ -1,5 +1,5 @@
-

Save CHannel Invitation

+

Make a Channel Invitation

diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.html b/src/app/nip76/nip76-settings/nip76-settings.component.html index 6916a239..4a1280be 100644 --- a/src/app/nip76/nip76-settings/nip76-settings.component.html +++ b/src/app/nip76/nip76-settings/nip76-settings.component.html @@ -77,7 +77,15 @@ Password Protected content_copy + verified_user +
    +
  • + + + +
  • +
@@ -118,7 +126,7 @@
Private Thread {{activeChannel.dkxPost.signingParent.nostrPubKey}}
- + diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.ts b/src/app/nip76/nip76-settings/nip76-settings.component.ts index 1a5c8113..6764d82a 100644 --- a/src/app/nip76/nip76-settings/nip76-settings.component.ts +++ b/src/app/nip76/nip76-settings/nip76-settings.component.ts @@ -128,16 +128,22 @@ export class Nip76SettingsComponent { const channel = await this.nip76Service.previewChannel(); if (channel) { this.activeChannelId = channel.dkxPost.signingParent.nostrPubKey; - if (this.tabIndex != 1) { - // this.viewChannelFollowers(channel); - this.router.navigate(['private-channels','following']); - } + if (this.tabIndex != 3) { + this.viewChannelNotes(channel); + } + } + } + + shouldRSVP(channel: PrivateChannel) { + if (channel.invitation?.pointer?.docIndex) { + return !channel.rsvps.find(x => x.content.pubkey === this.wallet.ownerPubKey); } + return false; } - async follow(channel: PrivateChannel) { - this.nip76Service.saveFollowing(channel) - this.snackBar.open(`You are now following this channel.`, 'Hide', { + async rsvp(channel: PrivateChannel) { + this.nip76Service.saveRSVP(channel) + this.snackBar.open(`Thank you for your RSVP to this channel.`, 'Hide', { duration: 3000, horizontalPosition: 'center', verticalPosition: 'bottom', diff --git a/src/app/nip76/nip76.service.ts b/src/app/nip76/nip76.service.ts index f1dad99b..af9e3c2a 100644 --- a/src/app/nip76/nip76.service.ts +++ b/src/app/nip76/nip76.service.ts @@ -1,10 +1,13 @@ import { Injectable } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { MatSnackBar } from '@angular/material/snack-bar'; +import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar'; import { bech32 } from '@scure/base'; -import { FollowDocument, Invitation, nip19Extension, Nip76Wallet, Nip76WebWalletStorage, PostDocument, PrivateChannel } from 'animiq-nip76-tools'; +import { + HDKIndex, HDKIndexType, Invitation, nip19Extension, Nip76Wallet, + Nip76WebWalletStorage, PostDocument, PrivateChannel, walletRsvpDocumentsOffset +} from 'animiq-nip76-tools'; import * as nostrTools from 'nostr-tools'; -import { Subject } from 'rxjs'; +import { firstValueFrom, Subject } from 'rxjs'; import { DataService } from '../services/data'; import { NostrEvent, NostrRelaySubscription } from '../services/interfaces'; import { ProfileService } from '../services/profile'; @@ -21,6 +24,12 @@ interface PrivateChannelWithRelaySub extends PrivateChannel { channelSubscription?: NostrRelaySubscription; } +const defaultSnackBarOpts: MatSnackBarConfig = { + duration: 3000, + horizontalPosition: 'center', + verticalPosition: 'bottom', +}; + @Injectable({ providedIn: 'root' }) @@ -64,15 +73,11 @@ export class Nip76Service { if (prvkey) { resolve(prvkey); } else { - this.snackBar.open(`Unable to decrypt data. Probably wrong password. Try again.`, 'Hide', { - duration: 3000, - horizontalPosition: 'center', - verticalPosition: 'bottom', - }); + this.snackBar.open(`Unable to access user private key. Probably wrong password. Try again.`, 'Hide', defaultSnackBarOpts); reject(); } } else { - reject(); + reject('Unable to access user private key.'); } }); }); @@ -106,40 +111,37 @@ export class Nip76Service { } loadDocuments(start = 1, length = 20) { - const authors = this.wallet.documentsIndex.getDocKeys(start, length); + const channelPubkeys = this.wallet.documentsIndex.getDocKeys(start, length); + const invitePubkeys = this.wallet.documentsIndex.getDocKeys(start + walletRsvpDocumentsOffset, length); if (this.documentsSubscription) { this.relayService.unsubscribe(this.documentsSubscription.id); } const privateNotes$ = new Subject(); privateNotes$.subscribe(async nostrEvent => { - const docIndex = authors.findIndex(x => x === nostrEvent.pubkey) + start; - const doc = await this.wallet.documentsIndex.readEvent(nostrEvent, docIndex); - if (doc instanceof PrivateChannel) { //} && doc.hdkIndex === this.wallet.documentsIndex) { - // const keyset = this.wallet.documentsIndex.getKeysFromIndex(sequentialIndex); - // doc.hdkIndex = new HDKIndex(HDKIndexType.TimeBased, keyset.signingKey!, keyset.cryptoKey); - this.loadChannel(doc); + let docIndex = channelPubkeys.findIndex(x => x === nostrEvent.pubkey) + start; + if (docIndex) { + const doc = await this.wallet.documentsIndex.readEvent(nostrEvent, docIndex) as PrivateChannel; + if (doc) { + this.loadChannel(doc); + } + } else { + docIndex = invitePubkeys.findIndex(x => x === nostrEvent.pubkey) + start + walletRsvpDocumentsOffset; + const doc = await this.wallet.documentsIndex.readEvent(nostrEvent, docIndex) as Invitation; + if (doc) { + this.readInvitation(doc); + } } - // const followDocument = await this.wallet.documentsIndex.readEvent(nostrEvent) as FollowDocument; - // if (followDocument) { - // const pointer: nip19Extension.PrivateChannelPointer = { - // ownerPubKey: followDocument.content.owner, - // signingKey: hexToBytes(followDocument.content.signing_key), - // cryptoKey: hexToBytes(followDocument.content.crypto_key), - // relays: followDocument.content.relays - // } - // await this.loadFollowing(pointer); - // } }); - - this.documentsSubscription = this.relayService.subscribe([{ - authors, - kinds: [17761], - limit: length - }], `nip76Service.loadDocuments.${start}-${length}`, 'Replaceable', privateNotes$); + const filters = [ + { authors: channelPubkeys, kinds: [17761], limit: length }, + { authors: invitePubkeys, kinds: [17761], limit: length } + ]; + this.documentsSubscription = this.relayService.subscribe(filters, + `nip76Service.loadDocuments.${start}-${length}`, 'Replaceable', privateNotes$); } loadChannel(channel: PrivateChannelWithRelaySub, start = 1, length = 20) { - const invitePubs = channel.dkxInvite.getDocKeys(start, length); + const invitePubs = channel.dkxInvite?.getDocKeys(start, length); if (channel.channelSubscription) { this.relayService.unsubscribe(channel.channelSubscription.id); } @@ -147,23 +149,25 @@ export class Nip76Service { privateChannel$.subscribe(async nostrEvent => { if (channel.dkxPost.eventTag === nostrEvent.tags[0][1]) { await channel.dkxPost.readEvent(nostrEvent); - } else { + } else if (channel.dkxRsvp.eventTag === nostrEvent.tags[0][1]) { + await channel.dkxRsvp.readEvent(nostrEvent); + } else if (invitePubs) { const docIndex = invitePubs.findIndex(x => x === nostrEvent.pubkey) + start; await channel.dkxInvite.readEvent(nostrEvent, docIndex); } }); + const filters: nostrTools.Filter[] = [ + { '#e': [channel.dkxPost.eventTag], kinds: [17761], limit: length }, + { '#e': [channel.dkxRsvp.eventTag], kinds: [17761], limit: length }, + ]; + if (invitePubs) { + filters.push({ authors: invitePubs, kinds: [17761], limit: 100 }) + } channel.channelSubscription = this.relayService.subscribe( - [{ - '#e': [channel.dkxPost.eventTag], - kinds: [17761], - limit: length - }, - { - authors: invitePubs, - kinds: [17761], - limit: length - }], - `nip76Service.loadChannel.${channel.dkxPost.eventTag}`, 'Replaceable', privateChannel$ + filters, + `nip76Service.loadChannel.${channel.dkxPost.eventTag}`, + 'Replaceable', + privateChannel$ ); } @@ -180,9 +184,9 @@ export class Nip76Service { }); dialogRef.afterClosed().subscribe(async (result: AddChannelDialogData) => { if (result?.channelPointer) { - const following = await this.readInvitation(result.channelPointer, ''); - if (following) { - resolve(following); + const channel = await this.readChannelPointer(result.channelPointer, result.password!); + if (channel) { + resolve(channel); } else { reject(); } @@ -215,64 +219,78 @@ export class Nip76Service { }); } + async readInvitation(invite: Invitation): Promise { + const channelIndex = new HDKIndex(HDKIndexType.Singleton, invite.content.signingParent!, invite.content.cryptoParent!); + const channelIndex$ = new Subject(); + const channelIndexSub = this.relayService.subscribe( + [{ authors: [channelIndex.signingParent.nostrPubKey], kinds: [17761], limit: 1 }], + `nip76Service.readInvitation.${channelIndex.signingParent.nostrPubKey}`, 'Replaceable', channelIndex$ + ); + const nostrEvent = await firstValueFrom(channelIndex$); + this.relayService.unsubscribe(channelIndexSub.id); + if (nostrEvent) { + const channel = await channelIndex.readEvent(nostrEvent) as PrivateChannel; + if (channel) { + channel.invitation = invite; + const exisitng = this.wallet.documentsIndex.documents.find(x => x.nostrEvent.id === nostrEvent.id) as PrivateChannel; + if (exisitng) { + channel.dkxPost.documents = exisitng.dkxPost.documents; + channel.dkxRsvp.documents = exisitng.dkxRsvp.documents; + channel.dkxInvite = exisitng.dkxInvite; + const index = this.wallet.documentsIndex.documents.findIndex(x => x.nostrEvent.id === nostrEvent.id); + this.wallet.documentsIndex.documents[index] = channel; + } else { + this.wallet.documentsIndex.documents.push(channel); + } + this.loadChannel(channel); + return channel; + } else { + this.snackBar.open(`Unable to read contents the channel pointer keyset.`, 'Hide', defaultSnackBarOpts); + } + } else { + this.snackBar.open(`Unable to locate the channel pointer keyset.`, 'Hide', defaultSnackBarOpts); + } + return undefined; + } + + async readChannelPointer(channelPointer: string, secret?: string) { - private async readInvitation(channelPointer: string | nip19Extension.PrivateChannelPointer, secret?: string | Uint8Array[]) { - let pointer: nip19Extension.PrivateChannelPointer; - if (typeof channelPointer === 'string') { + try { const words = bech32.decode(channelPointer, 5000).words; - const pointerType = Uint8Array.from(bech32.fromWords(words))[0] as nip19Extension.PointerType; - if((pointerType & nip19Extension.PointerType.SharedSecret) == nip19Extension.PointerType.SharedSecret) - { - - debugger + const pointerType = Uint8Array.from(bech32.fromWords(words))[0] as nip19Extension.PointerType; + if ((pointerType & nip19Extension.PointerType.SharedSecret) == nip19Extension.PointerType.SharedSecret) { + secret = await this.passwordDialog('Preview Private Invitation'); } const p = await nip19Extension.decode(channelPointer, secret!); - pointer = p.data as nip19Extension.PrivateChannelPointer; - } else { - pointer = channelPointer; - } - if (pointer) { - const channel = PrivateChannel.fromPointer(pointer) as PrivateChannelWithRelaySub; - if (channel.ownerPubKey === this.wallet.ownerPubKey) { - const message = 'Self owned channels should only be initialized from the wallet directly.'; - this.snackBar.open(message, 'Hide', { - duration: 3000, - horizontalPosition: 'center', - verticalPosition: 'bottom', - }); - return undefined; + const pointer = p.data as nip19Extension.PrivateChannelPointer; + + if (pointer) { + const inviteIndex = HDKIndex.fromChannelPointer(pointer); + const inviteIndex$ = new Subject(); + const inviteIndexSub = this.relayService.subscribe( + [{ authors: [inviteIndex.signingParent.nostrPubKey], kinds: [17761], limit: 1 }], + `nip76Service.readChannelPointer.${inviteIndex.signingParent.nostrPubKey}`, 'Replaceable', inviteIndex$ + ); + const nostrEvent = await firstValueFrom(inviteIndex$); + this.relayService.unsubscribe(inviteIndexSub.id); + if (nostrEvent) { + const invite = await inviteIndex.readEvent(nostrEvent) as Invitation; + if (invite) { + invite.pointer = pointer; + return this.readInvitation(invite); + } else { + this.snackBar.open(`Unable to read contents the channel pointer record.`, 'Hide', defaultSnackBarOpts); + } + } else { + this.snackBar.open(`Unable to locate channel pointer record.`, 'Hide', defaultSnackBarOpts); + } + } else { + this.snackBar.open(`Unable to decode channel pointer string.`, 'Hide', defaultSnackBarOpts); } - const privateChannel$ = new Subject(); - privateChannel$.subscribe(async nostrEvent => { - // const fchannel = await channel.hdkIndex.readEvent(nostrEvent, channel) as PrivateChannel; - // if (fchannel) { - // channel.content = fchannel.content; - // channel.ready = fchannel.ready; - // channel.hdkIndex.signingParent._chainCode = hexToBytes(fchannel.content.chain_sign!); - // channel.hdkIndex.cryptoParent._chainCode = hexToBytes(fchannel.content.chain_crypto!); - // channel.hdkIndex.eventTag = channel.hdkIndex.signingParent.deriveChildKey(0).deriveChildKey(0).pubKeyHash; - // this.relayService.unsubscribe(channel.channelSubscription!.id); - // this.loadChannel(channel); - // this.wallet.channels.push(channel); - // } - }); - channel.channelSubscription = this.relayService.subscribe( - [{ - authors: [channel.dkxPost.signingParent.nostrPubKey], - kinds: [17761], - limit: length - }], - `nip76Service.loadFollowing.${channel.dkxPost.eventTag}`, 'Replaceable', privateChannel$ - ); - return channel; - } else { - this.snackBar.open(`Unable to decode secure channel pointer.`, 'Hide', { - duration: 3000, - horizontalPosition: 'center', - verticalPosition: 'bottom', - }); - return undefined; + } catch (error) { + this.snackBar.open(`${error}`, 'Hide', defaultSnackBarOpts); } + return undefined; } async saveChannel(channel: PrivateChannel, privateKey?: string) { @@ -312,15 +330,16 @@ export class Nip76Service { async saveInvitation(channel: PrivateChannel, invitation: AddInvitationDialogData): Promise { const privateKey = await this.passwordDialog('Save Invitation'); const invite = new Invitation(); + invite.docIndex = channel.dkxInvite.documents.length + 1; invite.content = { kind: 1776, + docIndex: invite.docIndex, for: invitation.invitationType === 'pubkey' ? invitation.validPubkey : undefined, password: invitation.invitationType === 'password' ? invitation.password : undefined, pubkey: channel.dkxPost.signingParent.nostrPubKey, signingParent: channel.dkxPost.signingParent, cryptoParent: channel.dkxPost.cryptoParent, } - invite.docIndex = channel.dkxInvite.documents.length + 1; const event = await channel.dkxInvite.createEvent(invite, privateKey); await this.dataService.publishEvent(event); console.log(event); @@ -328,19 +347,21 @@ export class Nip76Service { } - async saveFollowing(following: PrivateChannel) { - const privateKey = await this.passwordDialog('Save Follow'); - const followDocument = new FollowDocument(); - // followDocument.content = { - // pubkey: this.wallet.ownerPubKey, - // kind: nostrTools.Kind.Contacts, - // signing_key: bytesToHex(following.hdkIndex.signingParent.publicKey), - // crypto_key: bytesToHex(following.hdkIndex.cryptoParent.publicKey), - // owner: following.ownerPubKey, - // relays: following.content.relays - // } - const event = await this.wallet.documentsIndex.createEvent(followDocument, privateKey); - await this.dataService.publishEvent(event); + async saveRSVP(channel: PrivateChannel) { + const privateKey = await this.passwordDialog('Save RSVP'); + if (channel.invitation.docIndex === undefined) { + channel.invitation.docIndex = this.wallet.rsvps.length + 1 + walletRsvpDocumentsOffset; + } + const event1 = await this.wallet.documentsIndex.createEvent(channel.invitation!, privateKey); + await this.dataService.publishEvent(event1); + const rsvp = new Invitation(); + rsvp.content = { + kind: 1776, + pubkey: this.wallet.ownerPubKey, + docIndex: channel.invitation.pointer?.docIndex + } + const event2 = await channel.dkxRsvp.createEvent(rsvp, privateKey); + await this.dataService.publishEvent(event2); return true; } From 27bb01f977ff0419e2d1111575f00d9537025922 Mon Sep 17 00:00:00 2001 From: d-krause Date: Thu, 30 Mar 2023 13:55:12 -0400 Subject: [PATCH 21/39] event deleting, copying invitation without saving, ability to delete invitations and rsvps --- .../nip76-add-invitation.component.html | 2 + .../nip76-add-invitation.component.ts | 36 +++++++++- .../nip76-settings.component.html | 32 ++++++--- .../nip76-settings.component.ts | 54 ++++++++++---- src/app/nip76/nip76.service.ts | 71 +++++++++++++------ 5 files changed, 147 insertions(+), 48 deletions(-) diff --git a/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.html b/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.html index aa956287..311b3c22 100644 --- a/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.html +++ b/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.html @@ -36,6 +36,8 @@

Make a Channel Invitation

+ diff --git a/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.ts b/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.ts index 3f28b578..d8a246d6 100644 --- a/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.ts +++ b/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.ts @@ -1,8 +1,12 @@ import { Component, Inject } from '@angular/core'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { Invitation, nip19Extension, PrivateChannel } from 'animiq-nip76-tools'; import { nip19 } from 'nostr-tools'; +import { defaultSnackBarOpts, Nip76Service } from '../nip76.service'; export interface AddInvitationDialogData { + channel: PrivateChannel; invitationType: 'pubkey' | 'password'; pubkey?: string; validPubkey?: string; @@ -19,7 +23,12 @@ export interface AddInvitationDialogData { export class Nip76AddInvitationComponent { error!: string; valid = false; - constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: AddInvitationDialogData) { + constructor( + private snackBar: MatSnackBar, + private nip76Service: Nip76Service, + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: AddInvitationDialogData, + ) { data.invitationType = 'pubkey'; } @@ -60,11 +69,34 @@ export class Nip76AddInvitationComponent { } } + async copyInviteWithoutSave() { + if (this.valid) { + let pointer: string; + const threadPointer = { + type: 0, + docIndex: -1, + signingKey: this.data.channel.dkxPost.signingParent!.publicKey, + signingChain: this.data.channel.dkxPost.signingParent!.chainCode, + cryptoKey: this.data.channel.dkxPost.cryptoParent.publicKey, + cryptoChain: this.data.channel.dkxPost.cryptoParent.chainCode, + }; + if (this.data.invitationType === 'password') { + pointer = await nip19Extension.nprivateChannelEncode(threadPointer, this.data.password!); + } else { + const privateKey = await this.nip76Service.passwordDialog('Save RSVP'); + pointer = await nip19Extension.nprivateChannelEncode(threadPointer, privateKey, this.data.validPubkey); + } + navigator.clipboard.writeText(pointer); + this.snackBar.open(`The invitation is now in your clipboard.`, 'Hide', defaultSnackBarOpts); + } + } + clearForm() { this.error = ''; this.valid = false; this.data = { - invitationType: this.data.invitationType + invitationType: this.data.invitationType, + channel: this.data.channel }; } } diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.html b/src/app/nip76/nip76-settings/nip76-settings.component.html index 4a1280be..af1dbb1c 100644 --- a/src/app/nip76/nip76-settings/nip76-settings.component.html +++ b/src/app/nip76/nip76-settings/nip76-settings.component.html @@ -48,10 +48,10 @@
-
{{channel.dkxPost.signingParent.nostrPubKey}}
- + delete + {{channel.dkxPost.signingParent.nostrPubKey}} +
@@ -60,7 +60,6 @@ -
@@ -76,13 +75,16 @@
  • Password Protected - content_copy - verified_user + content_copy + verified_user + delete + {{ createDate(invite.rsvps[0]) }}
    • - + verified_user + {{ createDate(rsvp) }}
    @@ -112,8 +114,18 @@ class="rounded-button" mat-flat-button color="primary" (click)="addChannel()">Add Channel
  • - - TBD + +
    No RSVPs Sent yet.
    +
    +
      +
    • + + delete + {{ createDate(rsvp) }} + +
    • +
    +
    diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.ts b/src/app/nip76/nip76-settings/nip76-settings.component.ts index 6764d82a..fd9ae956 100644 --- a/src/app/nip76/nip76-settings/nip76-settings.component.ts +++ b/src/app/nip76/nip76-settings/nip76-settings.component.ts @@ -3,11 +3,11 @@ import { FormBuilder, FormControl, Validators } from '@angular/forms'; import { MatSnackBar } from '@angular/material/snack-bar'; import { MatTabChangeEvent } from '@angular/material/tabs'; import { ActivatedRoute, Router } from '@angular/router'; -import { Invitation, Nip76Wallet, PostDocument, PrivateChannel } from 'animiq-nip76-tools'; +import { ContentDocument, Invitation, Nip76Wallet, PostDocument, PrivateChannel } from 'animiq-nip76-tools'; import { ApplicationState } from '../../services/applicationstate'; import { NavigationService } from '../../services/navigation'; import { UIService } from '../../services/ui'; -import { Nip76Service } from '../nip76.service'; +import { defaultSnackBarOpts, Nip76Service } from '../nip76.service'; @Component({ selector: 'app-nip76-settings', templateUrl: './nip76-settings.component.html', @@ -84,6 +84,7 @@ export class Nip76SettingsComponent { get editChannel(): PrivateChannel | null { return this._editChannel!; } + set editChannel(value: PrivateChannel | null) { this.cancelEdit(); this._editChannel = value; @@ -117,11 +118,7 @@ export class Nip76SettingsComponent { async copyKeys(invite: Invitation) { navigator.clipboard.writeText(await invite.getPointer()); - this.snackBar.open(`The invitation is now in your clipboard.`, 'Hide', { - duration: 3000, - horizontalPosition: 'center', - verticalPosition: 'bottom', - }); + this.snackBar.open(`The invitation is now in your clipboard.`, 'Hide', defaultSnackBarOpts); } async previewChannel() { @@ -135,19 +132,46 @@ export class Nip76SettingsComponent { } shouldRSVP(channel: PrivateChannel) { - if (channel.invitation?.pointer?.docIndex) { - return !channel.rsvps.find(x => x.content.pubkey === this.wallet.ownerPubKey); + // if(channel.ownerPubKey === this.wallet.ownerPubKey) return false; + if (channel.invitation?.pointer?.docIndex !== undefined) { + const huh = this.wallet.rsvps.filter(x => x.content.signingParent?.nostrPubKey === channel.dkxPost.signingParent.nostrPubKey); + return !huh.find(x => x.content.docIndex === channel.invitation?.content.docIndex); } return false; } async rsvp(channel: PrivateChannel) { this.nip76Service.saveRSVP(channel) - this.snackBar.open(`Thank you for your RSVP to this channel.`, 'Hide', { - duration: 3000, - horizontalPosition: 'center', - verticalPosition: 'bottom', - }); + this.snackBar.open(`Thank you for your RSVP to this channel.`, 'Hide', defaultSnackBarOpts); + } + + + async deleteInvitation(invite: Invitation) { + const privateKey = await this.nip76Service.passwordDialog('Delete Invitation'); + if (privateKey) { + if (await this.nip76Service.deleteDocument(invite, privateKey)) { + invite.dkxParent.documents.splice(invite.dkxParent.documents.indexOf(invite), 1); + } + } + } + + async deleteRSVP(rsvp: Invitation) { + const privateKey = await this.nip76Service.passwordDialog('Delete RSVP'); + if (privateKey) { + const channelRsvp = rsvp.channel?.rsvps.find(x => x.content.pubkey === this.wallet.ownerPubKey && x.content.docIndex === rsvp.content.docIndex); + if (await this.nip76Service.deleteDocument(rsvp, privateKey)) { + rsvp.dkxParent.documents.splice(rsvp.dkxParent.documents.indexOf(rsvp), 1); + if (channelRsvp) { + if (await this.nip76Service.deleteDocument(channelRsvp, privateKey)) { + channelRsvp.dkxParent.documents.splice(channelRsvp.dkxParent.documents.indexOf(channelRsvp), 1); + } + } + } + } + } + + createDate(doc: ContentDocument) { + return new Date(doc.nostrEvent.created_at * 1000) } addChannel() { @@ -173,7 +197,7 @@ export class Nip76SettingsComponent { } } - public addEmojiNote(event: { emoji: { native: any } }) { + addEmojiNote(event: { emoji: { native: any } }) { let startPos = (this.noteContent).nativeElement.selectionStart; let value = this.noteForm.controls.content.value; diff --git a/src/app/nip76/nip76.service.ts b/src/app/nip76/nip76.service.ts index af9e3c2a..76defea4 100644 --- a/src/app/nip76/nip76.service.ts +++ b/src/app/nip76/nip76.service.ts @@ -3,8 +3,10 @@ import { MatDialog } from '@angular/material/dialog'; import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar'; import { bech32 } from '@scure/base'; import { + ContentDocument, + HDKey, HDKIndex, HDKIndexType, Invitation, nip19Extension, Nip76Wallet, - Nip76WebWalletStorage, PostDocument, PrivateChannel, walletRsvpDocumentsOffset + Nip76WebWalletStorage, PostDocument, PrivateChannel, Versions, walletRsvpDocumentsOffset } from 'animiq-nip76-tools'; import * as nostrTools from 'nostr-tools'; import { firstValueFrom, Subject } from 'rxjs'; @@ -24,7 +26,7 @@ interface PrivateChannelWithRelaySub extends PrivateChannel { channelSubscription?: NostrRelaySubscription; } -const defaultSnackBarOpts: MatSnackBarConfig = { +export const defaultSnackBarOpts: MatSnackBarConfig = { duration: 3000, horizontalPosition: 'center', verticalPosition: 'bottom', @@ -59,7 +61,7 @@ export class Nip76Service { }); } - private async passwordDialog(actionPrompt: string): Promise { + async passwordDialog(actionPrompt: string): Promise { return new Promise((resolve, reject) => { const dialogRef = this.dialog.open(PasswordDialog, { data: { action: actionPrompt, password: '' }, @@ -200,7 +202,7 @@ export class Nip76Service { async addInvitation(channel: PrivateChannel): Promise { return new Promise((resolve, reject) => { const dialogRef = this.dialog.open(Nip76AddInvitationComponent, { - data: { channelPointer: '' }, + data: { channelPointer: '', channel }, maxWidth: '200vw', panelClass: 'full-width-dialog', }); @@ -265,25 +267,43 @@ export class Nip76Service { const pointer = p.data as nip19Extension.PrivateChannelPointer; if (pointer) { - const inviteIndex = HDKIndex.fromChannelPointer(pointer); - const inviteIndex$ = new Subject(); - const inviteIndexSub = this.relayService.subscribe( - [{ authors: [inviteIndex.signingParent.nostrPubKey], kinds: [17761], limit: 1 }], - `nip76Service.readChannelPointer.${inviteIndex.signingParent.nostrPubKey}`, 'Replaceable', inviteIndex$ - ); - const nostrEvent = await firstValueFrom(inviteIndex$); - this.relayService.unsubscribe(inviteIndexSub.id); - if (nostrEvent) { - const invite = await inviteIndex.readEvent(nostrEvent) as Invitation; - if (invite) { - invite.pointer = pointer; - return this.readInvitation(invite); + if ((pointer.type & nip19Extension.PointerType.FullKeySet) === nip19Extension.PointerType.FullKeySet) { + const signingParent = new HDKey({ publicKey: pointer.signingKey, chainCode: pointer.signingChain, version: Versions.nip76API1 }); + const cryptoParent = new HDKey({ publicKey: pointer.cryptoKey, chainCode: pointer.cryptoChain, version: Versions.nip76API1 }); + const invite = new Invitation(); + pointer.docIndex = -1; + invite.pointer = pointer; + invite.content = { + kind: 1776, + pubkey: signingParent.nostrPubKey, + docIndex: pointer.docIndex, + signingParent, + cryptoParent + }; + invite.ready = true; + return this.readInvitation(invite); + } else { + const inviteIndex = HDKIndex.fromChannelPointer(pointer); + const inviteIndex$ = new Subject(); + const inviteIndexSub = this.relayService.subscribe( + [{ authors: [inviteIndex.signingParent.nostrPubKey], kinds: [17761], limit: 1 }], + `nip76Service.readChannelPointer.${inviteIndex.signingParent.nostrPubKey}`, 'Replaceable', inviteIndex$ + ); + const nostrEvent = await firstValueFrom(inviteIndex$); + this.relayService.unsubscribe(inviteIndexSub.id); + if (nostrEvent) { + const invite = await inviteIndex.readEvent(nostrEvent) as Invitation; + if (invite) { + invite.pointer = pointer; + return this.readInvitation(invite); + } else { + this.snackBar.open(`Unable to read contents the channel pointer record.`, 'Hide', defaultSnackBarOpts); + } } else { - this.snackBar.open(`Unable to read contents the channel pointer record.`, 'Hide', defaultSnackBarOpts); + this.snackBar.open(`Unable to locate channel pointer record.`, 'Hide', defaultSnackBarOpts); } - } else { - this.snackBar.open(`Unable to locate channel pointer record.`, 'Hide', defaultSnackBarOpts); } + } else { this.snackBar.open(`Unable to decode channel pointer string.`, 'Hide', defaultSnackBarOpts); } @@ -342,7 +362,6 @@ export class Nip76Service { } const event = await channel.dkxInvite.createEvent(invite, privateKey); await this.dataService.publishEvent(event); - console.log(event); return invite; } @@ -365,6 +384,16 @@ export class Nip76Service { return true; } + async deleteDocument(doc: ContentDocument, privateKey?: string) { + // if(doc.ownerPubKey !== this.wallet.ownerPubKey) { + // this.snackBar.open(`Cannot delete another user's document.`, 'Hide', defaultSnackBarOpts); + // return false; + // } + privateKey = privateKey || await this.passwordDialog('Delete Document'); + const event = await doc.dkxParent.createDeleteEvent(doc, privateKey); + await this.dataService.publishEvent(event); + return true; + } } From 475fcabe1e3c023a3024989a75224527045b9d47 Mon Sep 17 00:00:00 2001 From: d-krause Date: Thu, 30 Mar 2023 17:52:59 -0400 Subject: [PATCH 22/39] new rsvp class resembles a PrivateChannelPointer - channel owner can now revoke invitations --- .../nip76-settings.component.html | 15 +++- .../nip76-settings.component.ts | 12 +-- src/app/nip76/nip76.service.ts | 90 ++++++++++++------- 3 files changed, 76 insertions(+), 41 deletions(-) diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.html b/src/app/nip76/nip76-settings/nip76-settings.component.html index af1dbb1c..0a9249b0 100644 --- a/src/app/nip76/nip76-settings/nip76-settings.component.html +++ b/src/app/nip76/nip76-settings/nip76-settings.component.html @@ -127,11 +127,22 @@
    - +
    Private Thread {{activeChannel.dkxPost.signingParent.nostrPubKey}}
    -
    +
    +
    No RSVPs Received yet.
    +
    +
      +
    • + + delete + {{ createDate(rsvp) }} + +
    • +
    +
    diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.ts b/src/app/nip76/nip76-settings/nip76-settings.component.ts index fd9ae956..b2cffaf1 100644 --- a/src/app/nip76/nip76-settings/nip76-settings.component.ts +++ b/src/app/nip76/nip76-settings/nip76-settings.component.ts @@ -3,7 +3,7 @@ import { FormBuilder, FormControl, Validators } from '@angular/forms'; import { MatSnackBar } from '@angular/material/snack-bar'; import { MatTabChangeEvent } from '@angular/material/tabs'; import { ActivatedRoute, Router } from '@angular/router'; -import { ContentDocument, Invitation, Nip76Wallet, PostDocument, PrivateChannel } from 'animiq-nip76-tools'; +import { ContentDocument, Invitation, Nip76Wallet, PostDocument, PrivateChannel, Rsvp } from 'animiq-nip76-tools'; import { ApplicationState } from '../../services/applicationstate'; import { NavigationService } from '../../services/navigation'; import { UIService } from '../../services/ui'; @@ -132,10 +132,10 @@ export class Nip76SettingsComponent { } shouldRSVP(channel: PrivateChannel) { - // if(channel.ownerPubKey === this.wallet.ownerPubKey) return false; + if(channel.ownerPubKey === this.wallet.ownerPubKey) return false; if (channel.invitation?.pointer?.docIndex !== undefined) { - const huh = this.wallet.rsvps.filter(x => x.content.signingParent?.nostrPubKey === channel.dkxPost.signingParent.nostrPubKey); - return !huh.find(x => x.content.docIndex === channel.invitation?.content.docIndex); + const huh = this.wallet.rsvps.filter(x => x.pointer === channel.invitation?.pointer); + return huh.length === 0; } return false; } @@ -155,10 +155,10 @@ export class Nip76SettingsComponent { } } - async deleteRSVP(rsvp: Invitation) { + async deleteRSVP(rsvp: Rsvp) { const privateKey = await this.nip76Service.passwordDialog('Delete RSVP'); if (privateKey) { - const channelRsvp = rsvp.channel?.rsvps.find(x => x.content.pubkey === this.wallet.ownerPubKey && x.content.docIndex === rsvp.content.docIndex); + const channelRsvp = rsvp.channel?.rsvps.find(x => x.content.pubkey === this.wallet.ownerPubKey && x.content.pointerDocIndex === rsvp.content.pointerDocIndex); if (await this.nip76Service.deleteDocument(rsvp, privateKey)) { rsvp.dkxParent.documents.splice(rsvp.dkxParent.documents.indexOf(rsvp), 1); if (channelRsvp) { diff --git a/src/app/nip76/nip76.service.ts b/src/app/nip76/nip76.service.ts index 76defea4..f5cc0358 100644 --- a/src/app/nip76/nip76.service.ts +++ b/src/app/nip76/nip76.service.ts @@ -6,7 +6,7 @@ import { ContentDocument, HDKey, HDKIndex, HDKIndexType, Invitation, nip19Extension, Nip76Wallet, - Nip76WebWalletStorage, PostDocument, PrivateChannel, Versions, walletRsvpDocumentsOffset + Nip76WebWalletStorage, PostDocument, PrivateChannel, Rsvp, Versions, walletRsvpDocumentsOffset } from 'animiq-nip76-tools'; import * as nostrTools from 'nostr-tools'; import { firstValueFrom, Subject } from 'rxjs'; @@ -128,9 +128,23 @@ export class Nip76Service { } } else { docIndex = invitePubkeys.findIndex(x => x === nostrEvent.pubkey) + start + walletRsvpDocumentsOffset; - const doc = await this.wallet.documentsIndex.readEvent(nostrEvent, docIndex) as Invitation; - if (doc) { - this.readInvitation(doc); + const doc = await this.wallet.documentsIndex.readEvent(nostrEvent, docIndex); + if (doc && doc instanceof Rsvp) { + const pointer: nip19Extension.PrivateChannelPointer = { + type: doc.content.type, + docIndex: doc.content.pointerDocIndex, + signingKey: doc.content.signingKey, + cryptoKey: doc.content.cryptoKey + }; + doc.pointer = pointer; + const rsvpIndex = HDKIndex.fromChannelPointer(pointer); + this.readChannelIndex(rsvpIndex, pointer); + // } else { + // const privateKey = await this.passwordDialog('Delete unwanted document'); + // const ddoc = new ContentDocument(); + // ddoc.nostrEvent = nostrEvent; + // ddoc.docIndex = docIndex; + // await this.deleteDocument(ddoc, privateKey); } } }); @@ -152,7 +166,12 @@ export class Nip76Service { if (channel.dkxPost.eventTag === nostrEvent.tags[0][1]) { await channel.dkxPost.readEvent(nostrEvent); } else if (channel.dkxRsvp.eventTag === nostrEvent.tags[0][1]) { - await channel.dkxRsvp.readEvent(nostrEvent); + const rsvp = await channel.dkxRsvp.readEvent(nostrEvent); + // if (rsvp && (rsvp.content.kind != 1777 || rsvp instanceof Invitation)) { + // channel.dkxRsvp.documents.splice(channel.dkxRsvp.documents.findIndex(x => rsvp), 1); + // const privateKey = await this.passwordDialog('Delete unwanted document'); + // await this.deleteDocument(rsvp, privateKey); + // } } else if (invitePubs) { const docIndex = invitePubs.findIndex(x => x === nostrEvent.pubkey) + start; await channel.dkxInvite.readEvent(nostrEvent, docIndex); @@ -284,26 +303,8 @@ export class Nip76Service { return this.readInvitation(invite); } else { const inviteIndex = HDKIndex.fromChannelPointer(pointer); - const inviteIndex$ = new Subject(); - const inviteIndexSub = this.relayService.subscribe( - [{ authors: [inviteIndex.signingParent.nostrPubKey], kinds: [17761], limit: 1 }], - `nip76Service.readChannelPointer.${inviteIndex.signingParent.nostrPubKey}`, 'Replaceable', inviteIndex$ - ); - const nostrEvent = await firstValueFrom(inviteIndex$); - this.relayService.unsubscribe(inviteIndexSub.id); - if (nostrEvent) { - const invite = await inviteIndex.readEvent(nostrEvent) as Invitation; - if (invite) { - invite.pointer = pointer; - return this.readInvitation(invite); - } else { - this.snackBar.open(`Unable to read contents the channel pointer record.`, 'Hide', defaultSnackBarOpts); - } - } else { - this.snackBar.open(`Unable to locate channel pointer record.`, 'Hide', defaultSnackBarOpts); - } + return this.readChannelIndex(inviteIndex, pointer); } - } else { this.snackBar.open(`Unable to decode channel pointer string.`, 'Hide', defaultSnackBarOpts); } @@ -313,6 +314,28 @@ export class Nip76Service { return undefined; } + async readChannelIndex(inviteIndex: HDKIndex, pointer: nip19Extension.PrivateChannelPointer) { + const inviteIndex$ = new Subject(); + const inviteIndexSub = this.relayService.subscribe( + [{ authors: [inviteIndex.signingParent.nostrPubKey], kinds: [17761], limit: 1 }], + `nip76Service.readChannelIndex.${inviteIndex.signingParent.nostrPubKey}`, 'Replaceable', inviteIndex$ + ); + const nostrEvent = await firstValueFrom(inviteIndex$); + this.relayService.unsubscribe(inviteIndexSub.id); + if (nostrEvent) { + const invite = await inviteIndex.readEvent(nostrEvent) as Invitation; + if (invite) { + invite.pointer = pointer; + return this.readInvitation(invite); + } else { + this.snackBar.open(`Unable to read contents the channel pointer record.`, 'Hide', defaultSnackBarOpts); + } + } else { + this.snackBar.open(`Unable to locate channel pointer record.`, 'Hide', defaultSnackBarOpts); + } + return undefined; + } + async saveChannel(channel: PrivateChannel, privateKey?: string) { privateKey = privateKey || await this.passwordDialog('Save Channel Details'); const ev = await this.wallet.documentsIndex.createEvent(channel, privateKey); @@ -368,24 +391,25 @@ export class Nip76Service { async saveRSVP(channel: PrivateChannel) { const privateKey = await this.passwordDialog('Save RSVP'); - if (channel.invitation.docIndex === undefined) { - channel.invitation.docIndex = this.wallet.rsvps.length + 1 + walletRsvpDocumentsOffset; - } - const event1 = await this.wallet.documentsIndex.createEvent(channel.invitation!, privateKey); - await this.dataService.publishEvent(event1); - const rsvp = new Invitation(); + const rsvp = new Rsvp(); + rsvp.docIndex = channel.invitation.docIndex || (this.wallet.rsvps.length + 1 + walletRsvpDocumentsOffset); rsvp.content = { - kind: 1776, + kind: 1777, pubkey: this.wallet.ownerPubKey, - docIndex: channel.invitation.pointer?.docIndex + pointerDocIndex: channel.invitation.pointer.docIndex, + type: channel.invitation.pointer.type, + signingKey: channel.invitation.pointer.signingKey!, + cryptoKey: channel.invitation.pointer.cryptoKey!, } + const event1 = await this.wallet.documentsIndex.createEvent(rsvp, privateKey); + await this.dataService.publishEvent(event1); const event2 = await channel.dkxRsvp.createEvent(rsvp, privateKey); await this.dataService.publishEvent(event2); return true; } async deleteDocument(doc: ContentDocument, privateKey?: string) { - // if(doc.ownerPubKey !== this.wallet.ownerPubKey) { + // if(doc.content.pubkey !== this.wallet.ownerPubKey) { // this.snackBar.open(`Cannot delete another user's document.`, 'Hide', defaultSnackBarOpts); // return false; // } From b3e8dce685448174d49faee8d622fcca1d81a516 Mon Sep 17 00:00:00 2001 From: d-krause Date: Fri, 31 Mar 2023 20:47:54 -0400 Subject: [PATCH 23/39] new diagnostics component. handling invites and rsvps, suspension and re-instatement --- src/app/app.module.ts | 4 +- .../nip76-diagnostics.component.html | 19 +++ .../nip76-diagnostics.component.scss | 40 ++++++ .../nip76-diagnostics.component.spec.ts | 23 ++++ .../nip76-diagnostics.component.ts | 117 ++++++++++++++++++ .../nip76-event-thread.component.html | 1 + .../nip76-settings.component.html | 109 ++++++++++------ .../nip76-settings.component.scss | 8 ++ .../nip76-settings.component.ts | 30 +++-- src/app/nip76/nip76.service.ts | 64 +++++----- 10 files changed, 329 insertions(+), 86 deletions(-) create mode 100644 src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.html create mode 100644 src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.scss create mode 100644 src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.spec.ts create mode 100644 src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index e082690f..b8624c76 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -159,6 +159,7 @@ import { Nip76EventButtonsComponent } from './nip76/nip76-event-buttons/nip76-ev import { Nip76EventThreadComponent } from './nip76/nip76-event-thread/nip76-event-thread.component'; import { Nip76ContentComponent } from './nip76/nip76-content/nip76-event-content.component'; import { Nip76AddInvitationComponent } from './nip76/nip76-add-invitation/nip76-add-invitation.component'; +import { Nip76DiagnosticsComponent } from './nip76/nip76-diagnostics/nip76-diagnostics.component'; @NgModule({ declarations: [ @@ -255,7 +256,8 @@ import { Nip76AddInvitationComponent } from './nip76/nip76-add-invitation/nip76- Nip76EventButtonsComponent, Nip76EventThreadComponent, Nip76ContentComponent, - Nip76AddInvitationComponent + Nip76AddInvitationComponent, + Nip76DiagnosticsComponent ], imports: [ AboutModule, diff --git a/src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.html b/src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.html new file mode 100644 index 00000000..0bd62d71 --- /dev/null +++ b/src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.html @@ -0,0 +1,19 @@ + + +
    + content_copy + + + + +
    +
    +
    +
    \ No newline at end of file diff --git a/src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.scss b/src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.scss new file mode 100644 index 00000000..20eaba09 --- /dev/null +++ b/src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.scss @@ -0,0 +1,40 @@ +.diagnostics { + position: fixed; + z-index: 500; + width: 600px; + display: none; + overflow: hidden; + background-color: white; + padding: 8px; + box-shadow: 0 4px 1px -1px rgba(0,0,0,.5), + 0 1px 1px 0 rgba(0,0,0,.5), + 0 1px 6px 0 rgba(0,0,0,.5); +} + +.action-button-icon.active { + color: blueviolet; +} + +.json-content { + max-height: 300px; + overflow-y: scroll; + background-color: #efefef; + border: 1px solid gray; + border-radius: 4px; + padding: 4px; + overflow-wrap: break-word; + font-family: 'Courier New', Courier, monospace; + font-size: 12px; + white-space: pre-wrap; +} + +::ng-deep .json-content b { + color:blue; +} +.copy-json-button { + display: inline; + position: relative; + left: 556px; + top: 42px; + cursor: pointer; +} \ No newline at end of file diff --git a/src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.spec.ts b/src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.spec.ts new file mode 100644 index 00000000..84842f9f --- /dev/null +++ b/src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Nip76DiagnosticsComponent } from './nip76-diagnostics.component'; + +describe('Nip76DiagnosticsComponent', () => { + let component: Nip76DiagnosticsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ Nip76DiagnosticsComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(Nip76DiagnosticsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.ts b/src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.ts new file mode 100644 index 00000000..81e6c747 --- /dev/null +++ b/src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.ts @@ -0,0 +1,117 @@ +import { Component, ElementRef, HostListener, Input, ViewChild } from '@angular/core'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { ContentDocument } from 'animiq-nip76-tools'; +import { defaultSnackBarOpts } from '../nip76.service'; + +enum DiagType { + Event = 'Event', + Payload = 'Payload', + Content = 'Content', + Document = 'Document', +} + +@Component({ + selector: 'app-nip76-diagnostics', + templateUrl: './nip76-diagnostics.component.html', + styleUrls: ['./nip76-diagnostics.component.scss'] +}) +export class Nip76DiagnosticsComponent { + diagType = DiagType.Event; + DiagTypeEnum = DiagType; + delay = 190; + includePrivateData = false; + keepOpen = false; + timer!: NodeJS.Timeout | undefined; + @Input() + doc!: ContentDocument + @ViewChild('diagButton', { read: ElementRef }) + diagButton!: ElementRef; + @ViewChild('diagCard', { read: ElementRef }) + diagCard!: ElementRef; + + constructor( + private sanitizer: DomSanitizer, + private snackBar: MatSnackBar, + ) { } + + ngOnInit() { + this.diagType = DiagType.Event; + } + + @HostListener('mouseenter') onMouseEnter() { + this.timer = setTimeout(() => { + + let x = this.diagButton.nativeElement.getBoundingClientRect().left + this.diagButton.nativeElement.offsetWidth / 2; + let y = this.diagButton.nativeElement.getBoundingClientRect().top + this.diagButton.nativeElement.offsetHeight / 2; + + this.diagCard.nativeElement.style.display = 'block'; + this.diagCard.nativeElement.style.top = y + 'px'; + this.diagCard.nativeElement.style.left = (x - 600) + 'px'; + + const diagHeight = 400; + if (y + diagHeight > document.scrollingElement!.scrollHeight) { + y = document.scrollingElement!.scrollHeight - diagHeight; + this.diagCard.nativeElement.style.top = y + 'px'; + } + + }, this.delay) + } + + @HostListener('mouseleave') onMouseLeave() { + if (this.timer) clearTimeout(this.timer); + this.timer = undefined; + if (!this.keepOpen) + this.diagCard.nativeElement.style.display = 'none'; + } + + @HostListener('window:keydown', ['$event']) + @HostListener('window:keyup', ['$event']) + keyEventDown(event: KeyboardEvent) { + if (event.ctrlKey) { + this.includePrivateData = !this.includePrivateData; + } + if (event.shiftKey) { + this.keepOpen = !this.keepOpen; + } + } + + prettierJson(forCopy = false): SafeHtml | string { + const keys: string[] = []; + const ignoreKeys = ['channelSubscription', 'documents']; + const privateDataKeys = ['xpriv', "xpub", "wordset", "password", "signingKey", "cryptoKey"]; + const replacer = (k: string, v: any) => { + if (this.diagType === DiagType.Document && ignoreKeys.includes(k)) { + return undefined; + } + if (keys.indexOf(k) === -1) { + keys.push(k); + } + if (v && !this.includePrivateData && privateDataKeys.includes(k)) { + return '**MASKED**'; + } + return v; + }; + const obj = { + 'Event': this.doc.nostrEvent, + 'Payload': this.doc.payload, + 'Content': this.doc.content, + 'Document': this.doc, + }[this.diagType] || {}; + let json = JSON.stringify(obj, replacer, 2); + if (forCopy) { + return json; + } else { + keys.forEach(k => { + const regex = new RegExp(`"${k}":`, 'g'); + json = json.replace(regex, `"${k}":`); + }); + return this.sanitizer.bypassSecurityTrustHtml(json); + } + } + + copyJson() { + navigator.clipboard.writeText(this.prettierJson(true) as string); + this.snackBar.open(`The JSON is copied into your clipboard.`, 'Hide', defaultSnackBarOpts); + } +} diff --git a/src/app/nip76/nip76-event-thread/nip76-event-thread.component.html b/src/app/nip76/nip76-event-thread/nip76-event-thread.component.html index f6f2669a..f3d1aaa1 100644 --- a/src/app/nip76/nip76-event-thread/nip76-event-thread.component.html +++ b/src/app/nip76/nip76-event-thread/nip76-event-thread.component.html @@ -6,6 +6,7 @@ verified_user +
    diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.html b/src/app/nip76/nip76-settings/nip76-settings.component.html index 0a9249b0..e13593be 100644 --- a/src/app/nip76/nip76-settings/nip76-settings.component.html +++ b/src/app/nip76/nip76-settings/nip76-settings.component.html @@ -48,18 +48,19 @@
    - delete + delete {{channel.dkxPost.signingParent.nostrPubKey}} +
    - +
    @@ -71,28 +72,46 @@
    Invitations add_circle
    -
      -
    • - Password Protected - - content_copy - verified_user - delete - {{ createDate(invite.rsvps[0]) }} - -
        -
      • - - verified_user - {{ createDate(rsvp) }} - -
      • -
      -
    • -
    -
    -
    - + + + + Password Protected {{ !invite.content.cryptoParent ? " (Suspended)" : ""}} + + + + + (Suspended) + + +
    + + content_copy + verified_user + delete + thumb_down + thumb_up + {{ createDate(invite.rsvps[0]) }} + + +
    +
    + +
    + + verified_user + {{ createDate(rsvp) }} + + +
    +
    +
    +
    @@ -117,14 +136,20 @@
    No RSVPs Sent yet.
    - + +
    +
    @@ -134,14 +159,15 @@
    No RSVPs Received yet.
    -
      -
    • + +
      - delete + delete {{ createDate(rsvp) }} -
    • -
    + +
    +
    @@ -168,13 +194,14 @@
    - +
    - {{ post.nostrEvent.created_at | ago }} - - verified_user - + {{ post.nostrEvent.created_at | ago }} + + verified_user + +
    diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.scss b/src/app/nip76/nip76-settings/nip76-settings.component.scss index df410793..518e5a14 100644 --- a/src/app/nip76/nip76-settings/nip76-settings.component.scss +++ b/src/app/nip76/nip76-settings/nip76-settings.component.scss @@ -57,4 +57,12 @@ li.instruction { position: relative; left: 66px; top: 22px; + width: 300px; +} + +.action-icon { + margin-right: 4px; + cursor: pointer; + font-size: 14px; + display: inline; } \ No newline at end of file diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.ts b/src/app/nip76/nip76-settings/nip76-settings.component.ts index b2cffaf1..8d52f49e 100644 --- a/src/app/nip76/nip76-settings/nip76-settings.component.ts +++ b/src/app/nip76/nip76-settings/nip76-settings.component.ts @@ -33,10 +33,6 @@ export class Nip76SettingsComponent { return this.nip76Service.wallet; } - get readyPosts(): PostDocument[] { - return this.activeChannel ? this.activeChannel.posts : []; - } - get activeChannel(): PrivateChannel | undefined { return this.activeChannelId ? this.nip76Service.findChannel(this.activeChannelId) : undefined; } @@ -132,9 +128,9 @@ export class Nip76SettingsComponent { } shouldRSVP(channel: PrivateChannel) { - if(channel.ownerPubKey === this.wallet.ownerPubKey) return false; + if (channel.ownerPubKey === this.wallet.ownerPubKey) return false; if (channel.invitation?.pointer?.docIndex !== undefined) { - const huh = this.wallet.rsvps.filter(x => x.pointer === channel.invitation?.pointer); + const huh = this.wallet.rsvps.filter(x => x.content.pointerDocIndex === channel.invitation?.pointer.docIndex); return huh.length === 0; } return false; @@ -145,14 +141,13 @@ export class Nip76SettingsComponent { this.snackBar.open(`Thank you for your RSVP to this channel.`, 'Hide', defaultSnackBarOpts); } - - async deleteInvitation(invite: Invitation) { - const privateKey = await this.nip76Service.passwordDialog('Delete Invitation'); - if (privateKey) { - if (await this.nip76Service.deleteDocument(invite, privateKey)) { - invite.dkxParent.documents.splice(invite.dkxParent.documents.indexOf(invite), 1); - } + async deleteChannelRSVP(channel: PrivateChannel) { + if (channel.invitation?.pointer?.docIndex) { + const rsvp = this.wallet.rsvps.find(x => x.channel === channel + && x.content.pointerDocIndex === channel.invitation.pointer.docIndex) as Rsvp; + await this.deleteRSVP(rsvp); } + } async deleteRSVP(rsvp: Rsvp) { @@ -170,6 +165,15 @@ export class Nip76SettingsComponent { } } + async deleteInvitation(invite: Invitation) { + const privateKey = await this.nip76Service.passwordDialog('Delete Invitation'); + if (privateKey) { + if (await this.nip76Service.deleteDocument(invite, privateKey)) { + invite.dkxParent.documents.splice(invite.dkxParent.documents.indexOf(invite), 1); + } + } + } + createDate(doc: ContentDocument) { return new Date(doc.nostrEvent.created_at * 1000) } diff --git a/src/app/nip76/nip76.service.ts b/src/app/nip76/nip76.service.ts index f5cc0358..8eed5ed9 100644 --- a/src/app/nip76/nip76.service.ts +++ b/src/app/nip76/nip76.service.ts @@ -6,7 +6,7 @@ import { ContentDocument, HDKey, HDKIndex, HDKIndexType, Invitation, nip19Extension, Nip76Wallet, - Nip76WebWalletStorage, PostDocument, PrivateChannel, Rsvp, Versions, walletRsvpDocumentsOffset + Nip76WebWalletStorage, NostrKinds, PostDocument, PrivateChannel, Rsvp, Versions, walletRsvpDocumentsOffset } from 'animiq-nip76-tools'; import * as nostrTools from 'nostr-tools'; import { firstValueFrom, Subject } from 'rxjs'; @@ -138,13 +138,7 @@ export class Nip76Service { }; doc.pointer = pointer; const rsvpIndex = HDKIndex.fromChannelPointer(pointer); - this.readChannelIndex(rsvpIndex, pointer); - // } else { - // const privateKey = await this.passwordDialog('Delete unwanted document'); - // const ddoc = new ContentDocument(); - // ddoc.nostrEvent = nostrEvent; - // ddoc.docIndex = docIndex; - // await this.deleteDocument(ddoc, privateKey); + const channel = await this.readChannelIndex(rsvpIndex, pointer); } } }); @@ -164,17 +158,12 @@ export class Nip76Service { const privateChannel$ = new Subject(); privateChannel$.subscribe(async nostrEvent => { if (channel.dkxPost.eventTag === nostrEvent.tags[0][1]) { - await channel.dkxPost.readEvent(nostrEvent); + const post = await channel.dkxPost.readEvent(nostrEvent); } else if (channel.dkxRsvp.eventTag === nostrEvent.tags[0][1]) { const rsvp = await channel.dkxRsvp.readEvent(nostrEvent); - // if (rsvp && (rsvp.content.kind != 1777 || rsvp instanceof Invitation)) { - // channel.dkxRsvp.documents.splice(channel.dkxRsvp.documents.findIndex(x => rsvp), 1); - // const privateKey = await this.passwordDialog('Delete unwanted document'); - // await this.deleteDocument(rsvp, privateKey); - // } } else if (invitePubs) { const docIndex = invitePubs.findIndex(x => x === nostrEvent.pubkey) + start; - await channel.dkxInvite.readEvent(nostrEvent, docIndex); + const invite = await channel.dkxInvite.readEvent(nostrEvent, docIndex); } }); const filters: nostrTools.Filter[] = [ @@ -241,6 +230,10 @@ export class Nip76Service { } async readInvitation(invite: Invitation): Promise { + if(!invite.content.signingParent && !invite.content.cryptoParent){ + this.snackBar.open(`Encountered a suspended invitation from ${invite.ownerPubKey}`, 'Hide', defaultSnackBarOpts); + return undefined; + } const channelIndex = new HDKIndex(HDKIndexType.Singleton, invite.content.signingParent!, invite.content.cryptoParent!); const channelIndex$ = new Subject(); const channelIndexSub = this.relayService.subscribe( @@ -274,8 +267,7 @@ export class Nip76Service { return undefined; } - async readChannelPointer(channelPointer: string, secret?: string) { - + async readChannelPointer(channelPointer: string, secret?: string): Promise { try { const words = bech32.decode(channelPointer, 5000).words; const pointerType = Uint8Array.from(bech32.fromWords(words))[0] as nip19Extension.PointerType; @@ -293,7 +285,7 @@ export class Nip76Service { pointer.docIndex = -1; invite.pointer = pointer; invite.content = { - kind: 1776, + kind: NostrKinds.PrivateChannelInvitation, pubkey: signingParent.nostrPubKey, docIndex: pointer.docIndex, signingParent, @@ -314,7 +306,7 @@ export class Nip76Service { return undefined; } - async readChannelIndex(inviteIndex: HDKIndex, pointer: nip19Extension.PrivateChannelPointer) { + async readChannelIndex(inviteIndex: HDKIndex, pointer: nip19Extension.PrivateChannelPointer): Promise { const inviteIndex$ = new Subject(); const inviteIndexSub = this.relayService.subscribe( [{ authors: [inviteIndex.signingParent.nostrPubKey], kinds: [17761], limit: 1 }], @@ -326,7 +318,7 @@ export class Nip76Service { const invite = await inviteIndex.readEvent(nostrEvent) as Invitation; if (invite) { invite.pointer = pointer; - return this.readInvitation(invite); + return await this.readInvitation(invite); } else { this.snackBar.open(`Unable to read contents the channel pointer record.`, 'Hide', defaultSnackBarOpts); } @@ -375,7 +367,7 @@ export class Nip76Service { const invite = new Invitation(); invite.docIndex = channel.dkxInvite.documents.length + 1; invite.content = { - kind: 1776, + kind: NostrKinds.PrivateChannelInvitation, docIndex: invite.docIndex, for: invitation.invitationType === 'pubkey' ? invitation.validPubkey : undefined, password: invitation.invitationType === 'password' ? invitation.password : undefined, @@ -386,35 +378,45 @@ export class Nip76Service { const event = await channel.dkxInvite.createEvent(invite, privateKey); await this.dataService.publishEvent(event); return invite; + } + async resaveInvitation(channel: PrivateChannel, invite: Invitation, withKeys: boolean): Promise { + const privateKey = await this.passwordDialog('Revoke Invitation'); + invite.content.signingParent = withKeys ? channel.dkxPost.signingParent : undefined; + invite.content.cryptoParent = withKeys ? channel.dkxPost.cryptoParent : undefined; + const event = await channel.dkxInvite.createEvent(invite, privateKey); + await this.dataService.publishEvent(event); + return invite; } async saveRSVP(channel: PrivateChannel) { const privateKey = await this.passwordDialog('Save RSVP'); const rsvp = new Rsvp(); - rsvp.docIndex = channel.invitation.docIndex || (this.wallet.rsvps.length + 1 + walletRsvpDocumentsOffset); rsvp.content = { - kind: 1777, + kind: NostrKinds.PrivateChannelRSVP, pubkey: this.wallet.ownerPubKey, pointerDocIndex: channel.invitation.pointer.docIndex, type: channel.invitation.pointer.type, - signingKey: channel.invitation.pointer.signingKey!, - cryptoKey: channel.invitation.pointer.cryptoKey!, } - const event1 = await this.wallet.documentsIndex.createEvent(rsvp, privateKey); + const event1 = await channel.dkxRsvp.createEvent(rsvp, privateKey); await this.dataService.publishEvent(event1); - const event2 = await channel.dkxRsvp.createEvent(rsvp, privateKey); + + rsvp.docIndex = channel.invitation.docIndex || (this.wallet.rsvps.length + 1 + walletRsvpDocumentsOffset); + rsvp.content.signingKey = channel.invitation.pointer.signingKey; + rsvp.content.cryptoKey = channel.invitation.pointer.cryptoKey; + const event2 = await this.wallet.documentsIndex.createEvent(rsvp, privateKey); await this.dataService.publishEvent(event2); + return true; } async deleteDocument(doc: ContentDocument, privateKey?: string) { - // if(doc.content.pubkey !== this.wallet.ownerPubKey) { - // this.snackBar.open(`Cannot delete another user's document.`, 'Hide', defaultSnackBarOpts); - // return false; - // } privateKey = privateKey || await this.passwordDialog('Delete Document'); const event = await doc.dkxParent.createDeleteEvent(doc, privateKey); + if (doc.nostrEvent.pubkey !== event.pubkey) { + this.snackBar.open(`Cannot delete another user's document.`, 'Hide', defaultSnackBarOpts); + return false; + } await this.dataService.publishEvent(event); return true; } From ea6eb79338aefd946eda87feecdb11e79fa53ee4 Mon Sep 17 00:00:00 2001 From: d-krause Date: Sun, 2 Apr 2023 21:18:21 -0400 Subject: [PATCH 24/39] better ui flow --- src/app/app-routing.module.ts | 19 +- src/app/app.css | 3 + src/app/app.module.ts | 12 +- .../add-channel-dialog.component.html | 2 +- .../nip76-channel-notes.component.html | 41 +++ .../nip76-channel-notes.component.scss | 38 +++ .../nip76-channel-notes.component.spec.ts | 23 ++ .../nip76-channel-notes.component.ts | 76 +++++ .../nip76-channel.component.html | 172 +++++++++++ .../nip76-channel.component.scss | 121 ++++++++ .../nip76-channel.component.spec.ts | 23 ++ .../nip76-channel/nip76-channel.component.ts | 135 +++++++++ .../nip76-diagnostics.component.scss | 17 +- .../nip76-diagnostics.component.ts | 12 +- .../nip76-event-thread.component.html | 1 - .../nip76-rsvps-sent.component.html | 20 ++ .../nip76-rsvps-sent.component.scss | 0 .../nip76-rsvps-sent.component.spec.ts | 23 ++ .../nip76-rsvps-sent.component.ts | 43 +++ .../nip76-settings.component.html | 283 ++++-------------- .../nip76-settings.component.scss | 34 ++- .../nip76-settings.component.spec.ts | 10 +- .../nip76-settings.component.ts | 221 ++++---------- src/app/nip76/nip76.service.ts | 14 +- 24 files changed, 920 insertions(+), 423 deletions(-) create mode 100644 src/app/nip76/nip76-channel-notes/nip76-channel-notes.component.html create mode 100644 src/app/nip76/nip76-channel-notes/nip76-channel-notes.component.scss create mode 100644 src/app/nip76/nip76-channel-notes/nip76-channel-notes.component.spec.ts create mode 100644 src/app/nip76/nip76-channel-notes/nip76-channel-notes.component.ts create mode 100644 src/app/nip76/nip76-channel/nip76-channel.component.html create mode 100644 src/app/nip76/nip76-channel/nip76-channel.component.scss create mode 100644 src/app/nip76/nip76-channel/nip76-channel.component.spec.ts create mode 100644 src/app/nip76/nip76-channel/nip76-channel.component.ts create mode 100644 src/app/nip76/nip76-rsvps-sent/nip76-rsvps-sent.component.html create mode 100644 src/app/nip76/nip76-rsvps-sent/nip76-rsvps-sent.component.scss create mode 100644 src/app/nip76/nip76-rsvps-sent/nip76-rsvps-sent.component.spec.ts create mode 100644 src/app/nip76/nip76-rsvps-sent/nip76-rsvps-sent.component.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index a1be02c0..46d23c41 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -33,7 +33,7 @@ import { LoginComponent } from './connect/login/login'; import { CreateProfileComponent } from './connect/create/create'; import { EditorBadgesComponent } from './editor-badges/editor'; import { BadgeComponent } from './badge/badge'; -import { Nip76SettingsComponent } from './nip76/nip76-settings/nip76-settings.component'; +import { Nip76MainComponent } from './nip76/nip76-settings/nip76-settings.component'; const routes: Routes = [ { @@ -142,33 +142,24 @@ const routes: Routes = [ }, { path: 'private-channels', - component: Nip76SettingsComponent, + component: Nip76MainComponent, canActivate: [AuthGuard], resolve: { data: LoadingResolverService, }, }, { - path: 'private-channels/following', - component: Nip76SettingsComponent, + path: 'private-channels/sent-rsvps', + component: Nip76MainComponent, canActivate: [AuthGuard], data: { tabIndex: 1 }, resolve: { data: LoadingResolverService, }, }, - { - path: 'private-channels/:channelPubKey/followers', - component: Nip76SettingsComponent, - data: { tabIndex: 2 }, - canActivate: [AuthGuard], - resolve: { - data: LoadingResolverService, - }, - }, { path: 'private-channels/:channelPubKey/notes', - component: Nip76SettingsComponent, + component: Nip76MainComponent, canActivate: [AuthGuard], data: { tabIndex: 3 }, resolve: { diff --git a/src/app/app.css b/src/app/app.css index 3936a639..22906b4a 100644 --- a/src/app/app.css +++ b/src/app/app.css @@ -254,6 +254,9 @@ header { overscroll-behavior-y: contain; overflow-x: hidden; overflow-y: overlay !important; + + flex-direction: column; + display: flex; } .app-mediaplayer { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index b8624c76..0c01eecc 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -153,13 +153,16 @@ import { TagsComponent } from './shared/tags/tags'; import { BadgeComponent } from './badge/badge'; import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; import { DragScrollModule } from 'ngx-drag-scroll'; -import { Nip76SettingsComponent } from './nip76/nip76-settings/nip76-settings.component'; +import { Nip76MainComponent } from './nip76/nip76-settings/nip76-settings.component'; import { AddChannelDialog } from './nip76/nip76-add-channel-dialog/add-channel-dialog.component'; import { Nip76EventButtonsComponent } from './nip76/nip76-event-buttons/nip76-event-buttons.component'; import { Nip76EventThreadComponent } from './nip76/nip76-event-thread/nip76-event-thread.component'; import { Nip76ContentComponent } from './nip76/nip76-content/nip76-event-content.component'; import { Nip76AddInvitationComponent } from './nip76/nip76-add-invitation/nip76-add-invitation.component'; import { Nip76DiagnosticsComponent } from './nip76/nip76-diagnostics/nip76-diagnostics.component'; +import { Nip76ChannelHeaderComponent } from './nip76/nip76-channel/nip76-channel.component'; +import { Nip76ChannelNotesComponent } from './nip76/nip76-channel-notes/nip76-channel-notes.component'; +import { Nip76RsvpsSentComponent } from './nip76/nip76-rsvps-sent/nip76-rsvps-sent.component'; @NgModule({ declarations: [ @@ -251,13 +254,16 @@ import { Nip76DiagnosticsComponent } from './nip76/nip76-diagnostics/nip76-diagn BadgeCardComponent, TagsComponent, BadgeComponent, - Nip76SettingsComponent, + Nip76MainComponent, AddChannelDialog, Nip76EventButtonsComponent, Nip76EventThreadComponent, Nip76ContentComponent, Nip76AddInvitationComponent, - Nip76DiagnosticsComponent + Nip76DiagnosticsComponent, + Nip76ChannelHeaderComponent, + Nip76ChannelNotesComponent, + Nip76RsvpsSentComponent ], imports: [ AboutModule, diff --git a/src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.html b/src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.html index 7b111d5b..af2ea7d6 100644 --- a/src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.html +++ b/src/app/nip76/nip76-add-channel-dialog/add-channel-dialog.component.html @@ -1,5 +1,5 @@
    -

    Channel Preview

    +

    Read Channel Invitation

    key diff --git a/src/app/nip76/nip76-channel-notes/nip76-channel-notes.component.html b/src/app/nip76/nip76-channel-notes/nip76-channel-notes.component.html new file mode 100644 index 00000000..3341dc32 --- /dev/null +++ b/src/app/nip76/nip76-channel-notes/nip76-channel-notes.component.html @@ -0,0 +1,41 @@ +
    + + +
    +
    +
    + + What's on your mind? + + + + sentiment_satisfied +
    +
    +   + +
    +
    +
    + +
    + + {{ + post.nostrEvent.created_at | ago }} + + verified_user + + +
    + +
    + {{ item.key }} + {{ item.value }} +
    + + +
    +
    diff --git a/src/app/nip76/nip76-channel-notes/nip76-channel-notes.component.scss b/src/app/nip76/nip76-channel-notes/nip76-channel-notes.component.scss new file mode 100644 index 00000000..c966a45b --- /dev/null +++ b/src/app/nip76/nip76-channel-notes/nip76-channel-notes.component.scss @@ -0,0 +1,38 @@ + +:host { + display: flex; + flex-direction: column; + flex-grow: 1; +} + +// .top-menu { +// display: flex; +// flex-direction: row; +// flex-wrap: nowrap; +// justify-content: flex-start; +// } +// .top-menu-left { +// display: flex; +// flex-grow: 1; +// } + +.top-menu-right { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-end; +} + +.notes-wrapper { + height: 200px; + flex-grow: 1; + overflow-y: scroll; + // border: 1px solid red; +} + +.note-card { + margin-top: 0; + padding: 1em; + margin-bottom: 1em; + border-radius: 10px !important; +} \ No newline at end of file diff --git a/src/app/nip76/nip76-channel-notes/nip76-channel-notes.component.spec.ts b/src/app/nip76/nip76-channel-notes/nip76-channel-notes.component.spec.ts new file mode 100644 index 00000000..5740a288 --- /dev/null +++ b/src/app/nip76/nip76-channel-notes/nip76-channel-notes.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Nip76ChannelNotesComponent } from './nip76-channel-notes.component'; + +describe('Nip76ChannelNotesComponent', () => { + let component: Nip76ChannelNotesComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ Nip76ChannelNotesComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(Nip76ChannelNotesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/nip76/nip76-channel-notes/nip76-channel-notes.component.ts b/src/app/nip76/nip76-channel-notes/nip76-channel-notes.component.ts new file mode 100644 index 00000000..31f98908 --- /dev/null +++ b/src/app/nip76/nip76-channel-notes/nip76-channel-notes.component.ts @@ -0,0 +1,76 @@ +import { Component, Input, ViewChild } from '@angular/core'; +import { FormBuilder, FormControl, Validators } from '@angular/forms'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { Invitation, Nip76Wallet, PostDocument, PrivateChannel, Rsvp } from 'animiq-nip76-tools'; +import { CircleService } from 'src/app/services/circle'; +import { Circle, NostrProfileDocument } from 'src/app/services/interfaces'; +import { ProfileService } from 'src/app/services/profile'; +import { defaultSnackBarOpts, Nip76Service } from '../nip76.service'; + +@Component({ + selector: 'app-nip76-channel-notes', + templateUrl: './nip76-channel-notes.component.html', + styleUrls: ['./nip76-channel-notes.component.scss'] +}) +export class Nip76ChannelNotesComponent { + showNoteForm = false; + isEmojiPickerVisible = false; + @ViewChild('picker') picker: unknown; + @ViewChild('noteContent') noteContent?: FormControl; + noteForm = this.fb.group({ + content: ['', Validators.required], + expiration: [''], + dateControl: [], + }); + + @Input() + channel!: PrivateChannel; + + constructor( + private profiles: ProfileService, + private circleService: CircleService, + private snackBar: MatSnackBar, + private fb: FormBuilder, + public nip76Service: Nip76Service + ) { } + + get wallet(): Nip76Wallet { + return this.nip76Service.wallet; + } + + public trackByFn(index: number, item: PostDocument) { + return item.nostrEvent.id; + } + + addEmojiNote(event: { emoji: { native: any } }) { + let startPos = (this.noteContent).nativeElement.selectionStart; + let value = this.noteForm.controls.content.value; + + let parsedValue = value?.substring(0, startPos) + event.emoji.native + value?.substring(startPos, value.length); + this.noteForm.controls.content.setValue(parsedValue); + this.isEmojiPickerVisible = false; + + (this.noteContent).nativeElement.focus(); + } + + async saveNote() { + if (await this.nip76Service.saveNote(this.channel!, this.noteForm.controls.content.value!)) { + this.noteForm.reset(); + this.showNoteForm = false; + } + } + + shouldRSVP(channel: PrivateChannel) { + if (channel.ownerPubKey === this.wallet.ownerPubKey) return false; + if (channel.invitation?.pointer?.docIndex !== undefined) { + const huh = this.wallet.rsvps.filter(x => x.content.pointerDocIndex === channel.invitation?.pointer.docIndex); + return huh.length === 0; + } + return false; + } + + async rsvp(channel: PrivateChannel) { + this.nip76Service.saveRSVP(channel) + this.snackBar.open(`Thank you for your RSVP to this channel.`, 'Hide', defaultSnackBarOpts); + } +} diff --git a/src/app/nip76/nip76-channel/nip76-channel.component.html b/src/app/nip76/nip76-channel/nip76-channel.component.html new file mode 100644 index 00000000..47d7c455 --- /dev/null +++ b/src/app/nip76/nip76-channel/nip76-channel.component.html @@ -0,0 +1,172 @@ + +
    +
    + + + +
    + +
    + +
    +
    +
    +
    + + + + + +
    +
    + +
    +
    +
    Name
    +
    {{channel.content.name}}
    +
    About
    +
    {{channel.content.about}} 
    +
    +
    + + Name + + + + About + + + + Picture + + + + +
    +
    +
    +
    + +
    +
    + + + + Password Protected {{ !invite.content.cryptoParent ? " (Suspended)" : ""}} + + + + + (Suspended) + + +
    + + + + Copy + + + Suspend + + + Reinstate + + + Delete + + + +
    +
    +
    +
    + + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    No RSVPs Received yet.
    +
    + +
    + + + + Delete + + + +
    +
    +
    +
    +
    +
    + +
    +
    + \ No newline at end of file diff --git a/src/app/nip76/nip76-channel/nip76-channel.component.scss b/src/app/nip76/nip76-channel/nip76-channel.component.scss new file mode 100644 index 00000000..259c4f96 --- /dev/null +++ b/src/app/nip76/nip76-channel/nip76-channel.component.scss @@ -0,0 +1,121 @@ +:host { + flex-grow: 2; + display: flex; + flex-direction: column; +} + +.flex { + display: flex; +} + +.icon-large .profile-image { + width: 256px; + height: 256px; +} + +.icon-medium .profile-image { + object-fit: cover; + width: 128px; + height: 128px; + border-radius: 50%; +} + +.icon-thumbnail .profile-image { + object-fit: cover; + width: 64px; + height: 64px; + border-radius: 50%; + border-width: 2px; + margin-left: 2px; + margin-right: 2px; + margin-bottom: 2px; +} + +.icon-small .profile-image { + object-fit: cover; + width: 45px; + height: 45px; + border-radius: 50%; + border-width: 2px; + margin-left: 4px; + margin-right: 4px; + margin-bottom: 2px; +} + +.content-items div { + word-wrap: break-word; + line-break: anywhere; +} + +.byline { + font-size: 12px; + + .about { + font-style: italic; + } +} + +.byline .profile-image { + object-fit: cover; + width: 24px; + height: 24px; + border-radius: 50%; + border-width: 1px; + margin-left: 2px; + margin-right: 2px; + margin-bottom: 1px; + position: relative; + top: 8px; +} + +.rounded-button { + border-radius: 16px; + margin-top: 0.24em; + margin-right: 0.5em; + margin-bottom: 12px; +} + +.channel-items-list { + height: 240px; + overflow-y: scroll; + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: flex-start +} + +.channel-items-card { + margin: 0px 10px 10px 0px; + border-radius: 10px !important; + width: 400px; + display: flex; +} + +.main { + min-height: 140px; + // flex-grow: 1; +} + +.field-label { + font-weight: bold; + margin-top: 4px; +} + +.label-only-anchor { + position: relative; + left: 66px; + top: 22px; + width: 300px; +} + +.action-icon { + margin-right: 4px; + cursor: pointer; + font-size: 14px; + display: inline; +} + +.action-link { + cursor: pointer; + margin: 2px 8px 2px 0px; +} \ No newline at end of file diff --git a/src/app/nip76/nip76-channel/nip76-channel.component.spec.ts b/src/app/nip76/nip76-channel/nip76-channel.component.spec.ts new file mode 100644 index 00000000..73088e30 --- /dev/null +++ b/src/app/nip76/nip76-channel/nip76-channel.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Nip76ChannelHeaderComponent } from './nip76-channel.component'; + +describe('Nip76ChannelHeaderComponent', () => { + let component: Nip76ChannelHeaderComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ Nip76ChannelHeaderComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(Nip76ChannelHeaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/nip76/nip76-channel/nip76-channel.component.ts b/src/app/nip76/nip76-channel/nip76-channel.component.ts new file mode 100644 index 00000000..82886ef2 --- /dev/null +++ b/src/app/nip76/nip76-channel/nip76-channel.component.ts @@ -0,0 +1,135 @@ +import { Component, Input } from '@angular/core'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { ContentDocument, Invitation, Nip76Wallet, PostDocument, PrivateChannel, Rsvp } from 'animiq-nip76-tools'; +import { CircleService } from 'src/app/services/circle'; +import { Circle, NostrProfileDocument } from 'src/app/services/interfaces'; +import { ProfileService } from 'src/app/services/profile'; +import { defaultSnackBarOpts, Nip76Service } from '../nip76.service'; + +enum DisplayType { + Summary = 'Summary', + Edit = 'Edit', + RSVPs = 'RSVPs', + Invitations = 'Invitations', + Notes = 'Notes', +} + +@Component({ + selector: 'app-nip76-channel', + templateUrl: './nip76-channel.component.html', + styleUrls: ['./nip76-channel.component.scss'] +}) +export class Nip76ChannelHeaderComponent { + private _channel?: PrivateChannel; + private _displayType = DisplayType.Summary; + DisplayType = DisplayType; + imagePath = '/assets/profile.png'; + profileName = ''; + profile?: NostrProfileDocument; + circle?: Circle; + + @Input() displayName = true; + @Input() showDisplayOptions = true; + @Input() listType = 'list'; + @Input() iconSize = 'small'; + + constructor( + private profiles: ProfileService, + private circleService: CircleService, + private snackBar: MatSnackBar, + public nip76Service: Nip76Service + ) { } + + @Input() + set displayType(val: 'Summary' | 'Edit' | 'RSVPs' | 'Invitations' | 'Notes') { + this._displayType = DisplayType[val] + } + + get displayType(): DisplayType { + return this._displayType!; + } + + @Input() + set channel(val: PrivateChannel) { + this._channel = val; + if (val.editing) { + this._displayType = DisplayType.Edit; + } + this.profiles.getProfile(val.ownerPubKey).then(async (profile) => { + this.profile = profile; + await this.updateProfileDetails(); + }); + } + + get channel(): PrivateChannel { + return this._channel!; + } + + get wallet(): Nip76Wallet { + return this.nip76Service.wallet; + } + + get pubkey(): string { + return this.channel.dkxPost.signingParent.nostrPubKey; + } + + get isOwner(): boolean { + return this.channel.ownerPubKey === this.wallet.ownerPubKey; + } + + async updateProfileDetails() { + this.imagePath = this.channel.content.picture || this.profile?.picture || this.imagePath; + if (this.profile) { + this.profileName = this.profile.display_name || this.profile.name || this.profileName; + this.circle = await this.circleService.get(this.profile.circle); + } + } + + async saveChannel() { + const success = await this.nip76Service.saveChannel(this.channel!); + } + + cancelAdd() { + const index = this.wallet.documentsIndex.documents.findIndex(x => this.channel); + this.wallet.documentsIndex.documents.splice(index, 1); + } + + async copyKeys(invite: Invitation) { + navigator.clipboard.writeText(await invite.getPointer()); + this.snackBar.open(`The invitation is now in your clipboard.`, 'Hide', defaultSnackBarOpts); + } + + async deleteRSVP(rsvp: Rsvp) { + const privateKey = await this.nip76Service.passwordDialog('Delete RSVP'); + if (privateKey) { + const channelRsvp = rsvp.channel?.rsvps.find(x => x.content.pubkey === this.wallet.ownerPubKey && x.content.pointerDocIndex === rsvp.content.pointerDocIndex); + if (await this.nip76Service.deleteDocument(rsvp, privateKey)) { + rsvp.dkxParent.documents.splice(rsvp.dkxParent.documents.indexOf(rsvp), 1); + if (channelRsvp) { + if (await this.nip76Service.deleteDocument(channelRsvp, privateKey)) { + channelRsvp.dkxParent.documents.splice(channelRsvp.dkxParent.documents.indexOf(channelRsvp), 1); + } + } + } + } + } + + async deleteInvitation(invite: Invitation) { + const privateKey = await this.nip76Service.passwordDialog('Delete Invitation'); + if (privateKey) { + if (await this.nip76Service.deleteDocument(invite, privateKey)) { + invite.dkxParent.documents.splice(invite.dkxParent.documents.indexOf(invite), 1); + } + } + } + + async deleteChannelRSVP(channel: PrivateChannel) { + if (channel.invitation?.pointer?.docIndex) { + const rsvp = this.wallet.rsvps.find(x => x.channel === channel + && x.content.pointerDocIndex === channel.invitation.pointer.docIndex) as Rsvp; + await this.deleteRSVP(rsvp); + } + + } + +} diff --git a/src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.scss b/src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.scss index 20eaba09..e702c51a 100644 --- a/src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.scss +++ b/src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.scss @@ -1,3 +1,7 @@ +:host { + height: 0px; +} + .diagnostics { position: fixed; z-index: 500; @@ -6,9 +10,9 @@ overflow: hidden; background-color: white; padding: 8px; - box-shadow: 0 4px 1px -1px rgba(0,0,0,.5), - 0 1px 1px 0 rgba(0,0,0,.5), - 0 1px 6px 0 rgba(0,0,0,.5); + box-shadow: 0 4px 1px -1px rgba(0, 0, 0, .5), + 0 1px 1px 0 rgba(0, 0, 0, .5), + 0 1px 6px 0 rgba(0, 0, 0, .5); } .action-button-icon.active { @@ -24,13 +28,14 @@ padding: 4px; overflow-wrap: break-word; font-family: 'Courier New', Courier, monospace; - font-size: 12px; - white-space: pre-wrap; + font-size: 12px; + white-space: pre-wrap; } ::ng-deep .json-content b { - color:blue; + color: blue; } + .copy-json-button { display: inline; position: relative; diff --git a/src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.ts b/src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.ts index 81e6c747..362c95a7 100644 --- a/src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.ts +++ b/src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.ts @@ -39,7 +39,8 @@ export class Nip76DiagnosticsComponent { this.diagType = DiagType.Event; } - @HostListener('mouseenter') onMouseEnter() { + @HostListener('mouseenter') + onMouseEnter() { this.timer = setTimeout(() => { let x = this.diagButton.nativeElement.getBoundingClientRect().left + this.diagButton.nativeElement.offsetWidth / 2; @@ -54,11 +55,18 @@ export class Nip76DiagnosticsComponent { y = document.scrollingElement!.scrollHeight - diagHeight; this.diagCard.nativeElement.style.top = y + 'px'; } + const sidNavWidth = parseInt((document.getElementsByClassName('mat-sidenav-content')[0] as any).style.marginLeft); + const diagWidth = 600; + if (x - diagWidth < sidNavWidth) { + x = sidNavWidth; + this.diagCard.nativeElement.style.left = x + 'px'; + } }, this.delay) } - @HostListener('mouseleave') onMouseLeave() { + @HostListener('mouseleave') + onMouseLeave() { if (this.timer) clearTimeout(this.timer); this.timer = undefined; if (!this.keepOpen) diff --git a/src/app/nip76/nip76-event-thread/nip76-event-thread.component.html b/src/app/nip76/nip76-event-thread/nip76-event-thread.component.html index f3d1aaa1..7056349b 100644 --- a/src/app/nip76/nip76-event-thread/nip76-event-thread.component.html +++ b/src/app/nip76/nip76-event-thread/nip76-event-thread.component.html @@ -7,7 +7,6 @@ verified_user -
    diff --git a/src/app/nip76/nip76-rsvps-sent/nip76-rsvps-sent.component.html b/src/app/nip76/nip76-rsvps-sent/nip76-rsvps-sent.component.html new file mode 100644 index 00000000..570f4dee --- /dev/null +++ b/src/app/nip76/nip76-rsvps-sent/nip76-rsvps-sent.component.html @@ -0,0 +1,20 @@ +
    No RSVPs Sent yet.
    +
    + + + + Suspended + + +
    + + delete + + + + +
    +
    +
    \ No newline at end of file diff --git a/src/app/nip76/nip76-rsvps-sent/nip76-rsvps-sent.component.scss b/src/app/nip76/nip76-rsvps-sent/nip76-rsvps-sent.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/nip76/nip76-rsvps-sent/nip76-rsvps-sent.component.spec.ts b/src/app/nip76/nip76-rsvps-sent/nip76-rsvps-sent.component.spec.ts new file mode 100644 index 00000000..25e8111d --- /dev/null +++ b/src/app/nip76/nip76-rsvps-sent/nip76-rsvps-sent.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Nip76RsvpsSentComponent } from './nip76-rsvps-sent.component'; + +describe('Nip76RsvpsSentComponent', () => { + let component: Nip76RsvpsSentComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ Nip76RsvpsSentComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(Nip76RsvpsSentComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/nip76/nip76-rsvps-sent/nip76-rsvps-sent.component.ts b/src/app/nip76/nip76-rsvps-sent/nip76-rsvps-sent.component.ts new file mode 100644 index 00000000..7a7f0e72 --- /dev/null +++ b/src/app/nip76/nip76-rsvps-sent/nip76-rsvps-sent.component.ts @@ -0,0 +1,43 @@ +import { Component, Input, ViewChild } from '@angular/core'; +import { FormBuilder, FormControl, Validators } from '@angular/forms'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { Invitation, Nip76Wallet, PostDocument, PrivateChannel, Rsvp } from 'animiq-nip76-tools'; +import { CircleService } from 'src/app/services/circle'; +import { Circle, NostrProfileDocument } from 'src/app/services/interfaces'; +import { ProfileService } from 'src/app/services/profile'; +import { defaultSnackBarOpts, Nip76Service } from '../nip76.service'; + +@Component({ + selector: 'app-nip76-rsvps-sent', + templateUrl: './nip76-rsvps-sent.component.html', + styleUrls: ['./nip76-rsvps-sent.component.scss'] +}) +export class Nip76RsvpsSentComponent { + + constructor( + private profiles: ProfileService, + private circleService: CircleService, + private snackBar: MatSnackBar, + private fb: FormBuilder, + public nip76Service: Nip76Service + ) { } + + get wallet(): Nip76Wallet { + return this.nip76Service.wallet; + } + + async deleteRSVP(rsvp: Rsvp) { + const privateKey = await this.nip76Service.passwordDialog('Delete RSVP'); + if (privateKey) { + const channelRsvp = rsvp.channel?.rsvps.find(x => x.content.pubkey === this.wallet.ownerPubKey && x.content.pointerDocIndex === rsvp.content.pointerDocIndex); + if (await this.nip76Service.deleteDocument(rsvp, privateKey)) { + rsvp.dkxParent.documents.splice(rsvp.dkxParent.documents.indexOf(rsvp), 1); + if (channelRsvp) { + if (await this.nip76Service.deleteDocument(channelRsvp, privateKey)) { + channelRsvp.dkxParent.documents.splice(channelRsvp.dkxParent.documents.indexOf(channelRsvp), 1); + } + } + } + } + } +} diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.html b/src/app/nip76/nip76-settings/nip76-settings.component.html index e13593be..34618b46 100644 --- a/src/app/nip76/nip76-settings/nip76-settings.component.html +++ b/src/app/nip76/nip76-settings/nip76-settings.component.html @@ -1,224 +1,73 @@ -
    - - - - - -
    - -
    -

    NIP 76 Private Channels are not initialized for this profile yet.

    -

    To get started:

    -
      -
    1. - Randomize the keys. (OPTIONAL). -
    2. -
    3. - Save it to begin using. -
    4. -
    - -
    - - Index - - - - Name - - - - About - - -
    -
    +
    +
    + + +
    +
    + + + + + + +
    - - - -
    -
    - - -
    - - delete - {{channel.dkxPost.signingParent.nostrPubKey}} - - - - -
    -
    - - - - - -
    -
    -
    Name
    -
    {{channel.content.name}}
    -
    About
    -
    {{channel.content.about}} 
    -
    -
    Invitations - add_circle -
    - - - - Password Protected {{ !invite.content.cryptoParent ? " (Suspended)" : ""}} - - - - - (Suspended) - - -
    - - content_copy - verified_user - delete - thumb_down - thumb_up - {{ createDate(invite.rsvps[0]) }} - - -
    -
    - -
    - - verified_user - {{ createDate(rsvp) }} - - -
    -
    -
    -
    -
    -
    -
    - - Name - - - - About - - -
    -
    -
    +
    +
    +
    +

    NIP 76 Private Channels are not initialized for this profile yet.

    +

    To get started:

    +
      +
    1. + Randomize the keys. (OPTIONAL). +
    2. +
    3. + Save it to begin using. +
    4. +
    +
    + + Index + + + + Name + + + + About + +
    -
    - -
    - - -
    No RSVPs Sent yet.
    -
    - - - - Suspended - - -
    - - delete - {{ createDate(rsvp) }} - - -
    -
    -
    -
    - -
    -
    - Private Thread {{activeChannel.dkxPost.signingParent.nostrPubKey}}
    -
    -
    No RSVPs Received yet.
    -
    - +
    + +
    - - delete - {{ createDate(rsvp) }} - - + +
    -
    -
    + +
    +
    +
    + + +
    +
    +
    - - -
    -
    Private Thread {{activeChannel.dkxPost.signingParent.nostrPubKey}}
    -
    - - - -
    -
    -
    - - What's on your mind? - - - - sentiment_satisfied -
    -
    -   - -
    -
    - -
    - - {{ post.nostrEvent.created_at | ago }} - - verified_user - - - -
    - -
    - {{ item.key }} {{ item.value }} -
    - - -
    -
    -
    - - - - +
    \ No newline at end of file diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.scss b/src/app/nip76/nip76-settings/nip76-settings.component.scss index 518e5a14..4d27c80c 100644 --- a/src/app/nip76/nip76-settings/nip76-settings.component.scss +++ b/src/app/nip76/nip76-settings/nip76-settings.component.scss @@ -1,3 +1,14 @@ +:host { + padding: 1em; + display: flex; + flex-direction: column; + flex-grow: 1; +} +.flex-height { + display: flex; + flex-direction: column; + flex-grow: 1; +} .menu-link { display: inline-block; padding-right: 16px; @@ -62,7 +73,24 @@ li.instruction { .action-icon { margin-right: 4px; - cursor: pointer; - font-size: 14px; + cursor: pointer; + font-size: 14px; display: inline; -} \ No newline at end of file +} +.top-menu { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; +} +.top-menu-left { + display: flex; + flex-grow: 1; +} + +.top-menu-right { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-end; +} diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.spec.ts b/src/app/nip76/nip76-settings/nip76-settings.component.spec.ts index fc9f294b..79c95d94 100644 --- a/src/app/nip76/nip76-settings/nip76-settings.component.spec.ts +++ b/src/app/nip76/nip76-settings/nip76-settings.component.spec.ts @@ -1,18 +1,18 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Nip76SettingsComponent } from './nip76-settings.component'; +import { Nip76MainComponent } from './nip76-settings.component'; describe('PrivateThreadsComponent', () => { - let component: Nip76SettingsComponent; - let fixture: ComponentFixture; + let component: Nip76MainComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ Nip76SettingsComponent ] + declarations: [ Nip76MainComponent ] }) .compileComponents(); - fixture = TestBed.createComponent(Nip76SettingsComponent); + fixture = TestBed.createComponent(Nip76MainComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.ts b/src/app/nip76/nip76-settings/nip76-settings.component.ts index 8d52f49e..4669c23d 100644 --- a/src/app/nip76/nip76-settings/nip76-settings.component.ts +++ b/src/app/nip76/nip76-settings/nip76-settings.component.ts @@ -1,41 +1,28 @@ -import { Component, ViewChild } from '@angular/core'; -import { FormBuilder, FormControl, Validators } from '@angular/forms'; +import { Component } from '@angular/core'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { MatTabChangeEvent } from '@angular/material/tabs'; import { ActivatedRoute, Router } from '@angular/router'; -import { ContentDocument, Invitation, Nip76Wallet, PostDocument, PrivateChannel, Rsvp } from 'animiq-nip76-tools'; +import { Nip76Wallet, PrivateChannel } from 'animiq-nip76-tools'; import { ApplicationState } from '../../services/applicationstate'; import { NavigationService } from '../../services/navigation'; import { UIService } from '../../services/ui'; -import { defaultSnackBarOpts, Nip76Service } from '../nip76.service'; +import { Nip76Service } from '../nip76.service'; + +enum DisplayType { + GuestUser = 'GuestUser', + ChannelList = 'ChannelList', + SingleChannel = 'SingleChannel', + SentRSVPs = 'SentRSVPs', +} @Component({ - selector: 'app-nip76-settings', + selector: 'app-nip76', templateUrl: './nip76-settings.component.html', styleUrls: ['./nip76-settings.component.scss'] }) -export class Nip76SettingsComponent { +export class Nip76MainComponent { - tabIndex?: number; - showNoteForm = false; - private _editChannel: PrivateChannel | null = null; + DisplayType = DisplayType; + editChannel?: PrivateChannel; activeChannelId!: string | null; - // _activeChannel: PrivateChannel | undefined; - isEmojiPickerVisible = false; - @ViewChild('picker') picker: unknown; - @ViewChild('noteContent') noteContent?: FormControl; - noteForm = this.fb.group({ - content: ['', Validators.required], - expiration: [''], - dateControl: [], - }); - - get wallet(): Nip76Wallet { - return this.nip76Service.wallet; - } - - get activeChannel(): PrivateChannel | undefined { - return this.activeChannelId ? this.nip76Service.findChannel(this.activeChannelId) : undefined; - } constructor( private router: Router, @@ -45,21 +32,46 @@ export class Nip76SettingsComponent { public appState: ApplicationState, public ui: UIService, public nip76Service: Nip76Service, - private fb: FormBuilder, ) { } async ngOnInit() { this.activatedRoute.paramMap.subscribe(async (params) => { - this.tabIndex = this.activatedRoute.snapshot.data['tabIndex'] as number || 0; this.activeChannelId = params.get('channelPubKey'); + }); + } + + get displayType(): DisplayType { + if (!this.wallet || this.wallet.isGuest) { + return DisplayType.GuestUser; + } else { if (this.activeChannelId) { - if (this.tabIndex < 2) this.tabIndex = 3; + const channel = this.nip76Service.findChannel(this.activeChannelId); + if (channel) { + return DisplayType.SingleChannel; + } + } if( this.router.url.endsWith('/sent-rsvps')) { + return DisplayType.SentRSVPs; + } else { + return DisplayType.ChannelList; } - }); + } } - public trackByFn(index: number, item: PostDocument) { - return item.nostrEvent.id; + get wallet(): Nip76Wallet { + return this.nip76Service.wallet; + } + + get activeChannel(): PrivateChannel | undefined { + if (this.activeChannelId) { + if (!this.wallet.isGuest && this.wallet.isInSession) { + const channel = this.nip76Service.findChannel(this.activeChannelId); + if (channel) { + return channel; + } + } + this.listChannels(); + } + return undefined; } randomizeKey() { @@ -67,7 +79,7 @@ export class Nip76SettingsComponent { this.editChannel = this.wallet.channels[0]; this.editChannel.editing = true; this.editChannel.content.name = 'Example Channel 1'; - this.editChannel.content.about = 'My First Trace Resistant Channel 1'; + this.editChannel.content.about = 'My First Private Channel 1'; this.editChannel.ready = true; } @@ -77,145 +89,26 @@ export class Nip76SettingsComponent { location.reload(); } - get editChannel(): PrivateChannel | null { - return this._editChannel!; - } - - set editChannel(value: PrivateChannel | null) { - this.cancelEdit(); - this._editChannel = value; - } - - onTabChanged(event: MatTabChangeEvent) { - this.tabIndex = event.index; - switch (event.index) { - case 0: - this.router.navigate(['/private-channels']); - break; - case 1: - this.router.navigate(['/private-channels/following']); - break; - case 2: - this.viewChannelFollowers(this.wallet.channels[0]); - break; - case 3: - this.viewChannelNotes(this.wallet.channels[0]); - break; - } - } - - viewChannelNotes(channel: PrivateChannel) { - this.router.navigate(['private-channels', channel.dkxPost.signingParent.nostrPubKey, 'notes']); - } - - viewChannelFollowers(channel: PrivateChannel) { - this.router.navigate(['private-channels', channel.dkxPost.signingParent.nostrPubKey, 'followers']); - } - - async copyKeys(invite: Invitation) { - navigator.clipboard.writeText(await invite.getPointer()); - this.snackBar.open(`The invitation is now in your clipboard.`, 'Hide', defaultSnackBarOpts); - } - - async previewChannel() { - const channel = await this.nip76Service.previewChannel(); + async readInvitation() { + const channel = await this.nip76Service.readInvitationDialog(); if (channel) { this.activeChannelId = channel.dkxPost.signingParent.nostrPubKey; - if (this.tabIndex != 3) { - this.viewChannelNotes(channel); - } - } - } - - shouldRSVP(channel: PrivateChannel) { - if (channel.ownerPubKey === this.wallet.ownerPubKey) return false; - if (channel.invitation?.pointer?.docIndex !== undefined) { - const huh = this.wallet.rsvps.filter(x => x.content.pointerDocIndex === channel.invitation?.pointer.docIndex); - return huh.length === 0; - } - return false; - } - - async rsvp(channel: PrivateChannel) { - this.nip76Service.saveRSVP(channel) - this.snackBar.open(`Thank you for your RSVP to this channel.`, 'Hide', defaultSnackBarOpts); - } - - async deleteChannelRSVP(channel: PrivateChannel) { - if (channel.invitation?.pointer?.docIndex) { - const rsvp = this.wallet.rsvps.find(x => x.channel === channel - && x.content.pointerDocIndex === channel.invitation.pointer.docIndex) as Rsvp; - await this.deleteRSVP(rsvp); - } - - } - - async deleteRSVP(rsvp: Rsvp) { - const privateKey = await this.nip76Service.passwordDialog('Delete RSVP'); - if (privateKey) { - const channelRsvp = rsvp.channel?.rsvps.find(x => x.content.pubkey === this.wallet.ownerPubKey && x.content.pointerDocIndex === rsvp.content.pointerDocIndex); - if (await this.nip76Service.deleteDocument(rsvp, privateKey)) { - rsvp.dkxParent.documents.splice(rsvp.dkxParent.documents.indexOf(rsvp), 1); - if (channelRsvp) { - if (await this.nip76Service.deleteDocument(channelRsvp, privateKey)) { - channelRsvp.dkxParent.documents.splice(channelRsvp.dkxParent.documents.indexOf(channelRsvp), 1); - } - } - } - } - } - - async deleteInvitation(invite: Invitation) { - const privateKey = await this.nip76Service.passwordDialog('Delete Invitation'); - if (privateKey) { - if (await this.nip76Service.deleteDocument(invite, privateKey)) { - invite.dkxParent.documents.splice(invite.dkxParent.documents.indexOf(invite), 1); - } + this.router.navigate(['/private-channels', this.activeChannelId, 'notes']); } } - createDate(doc: ContentDocument) { - return new Date(doc.nostrEvent.created_at * 1000) - } - - addChannel() { - this.cancelEdit(); - let newChannel = this.wallet.createChannel() + createChannel() { + let newChannel = this.wallet.createChannel(); newChannel.ready = newChannel.editing = true; - this._editChannel = newChannel; - } - - cancelEdit() { - if (this._editChannel && this._editChannel.editing) { - this._editChannel.editing = false; - this._editChannel.ready = false; - } - this._editChannel = null; - } - - async saveChannel() { - this._editChannel!.editing = false; - const savedRemote = await this.nip76Service.saveChannel(this._editChannel!); - if (savedRemote) { - this._editChannel = null; - } + this.editChannel = newChannel; + this.router.navigate(['/private-channels']); } - addEmojiNote(event: { emoji: { native: any } }) { - let startPos = (this.noteContent).nativeElement.selectionStart; - let value = this.noteForm.controls.content.value; - - let parsedValue = value?.substring(0, startPos) + event.emoji.native + value?.substring(startPos, value.length); - this.noteForm.controls.content.setValue(parsedValue); - this.isEmojiPickerVisible = false; - - (this.noteContent).nativeElement.focus(); + listChannels() { + this.router.navigate(['/private-channels']); } - async saveNote() { - if (await this.nip76Service.saveNote(this.activeChannel!, this.noteForm.controls.content.value!)) { - this.noteForm.reset(); - this.showNoteForm = false; - } + sentRvps() { + this.router.navigate(['/private-channels', 'sent-rsvps']); } } diff --git a/src/app/nip76/nip76.service.ts b/src/app/nip76/nip76.service.ts index 8eed5ed9..c079e8d6 100644 --- a/src/app/nip76/nip76.service.ts +++ b/src/app/nip76/nip76.service.ts @@ -3,8 +3,7 @@ import { MatDialog } from '@angular/material/dialog'; import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar'; import { bech32 } from '@scure/base'; import { - ContentDocument, - HDKey, + ContentDocument, HDKey, getNowSeconds, HDKIndex, HDKIndexType, Invitation, nip19Extension, Nip76Wallet, Nip76WebWalletStorage, NostrKinds, PostDocument, PrivateChannel, Rsvp, Versions, walletRsvpDocumentsOffset } from 'animiq-nip76-tools'; @@ -185,7 +184,7 @@ export class Nip76Service { return this.wallet?.channels.find(x => pubkey === x.dkxPost.signingParent.nostrPubKey); } - async previewChannel(): Promise { + async readInvitationDialog(): Promise { return new Promise((resolve, reject) => { const dialogRef = this.dialog.open(AddChannelDialog, { data: { channelPointer: '' }, @@ -230,7 +229,7 @@ export class Nip76Service { } async readInvitation(invite: Invitation): Promise { - if(!invite.content.signingParent && !invite.content.cryptoParent){ + if (!invite.content.signingParent && !invite.content.cryptoParent) { this.snackBar.open(`Encountered a suspended invitation from ${invite.ownerPubKey}`, 'Hide', defaultSnackBarOpts); return undefined; } @@ -267,7 +266,7 @@ export class Nip76Service { return undefined; } - async readChannelPointer(channelPointer: string, secret?: string): Promise { + async readChannelPointer(channelPointer: string, secret?: string): Promise { try { const words = bech32.decode(channelPointer, 5000).words; const pointerType = Uint8Array.from(bech32.fromWords(words))[0] as nip19Extension.PointerType; @@ -306,7 +305,7 @@ export class Nip76Service { return undefined; } - async readChannelIndex(inviteIndex: HDKIndex, pointer: nip19Extension.PrivateChannelPointer): Promise { + async readChannelIndex(inviteIndex: HDKIndex, pointer: nip19Extension.PrivateChannelPointer): Promise { const inviteIndex$ = new Subject(); const inviteIndexSub = this.relayService.subscribe( [{ authors: [inviteIndex.signingParent.nostrPubKey], kinds: [17761], limit: 1 }], @@ -330,6 +329,7 @@ export class Nip76Service { async saveChannel(channel: PrivateChannel, privateKey?: string) { privateKey = privateKey || await this.passwordDialog('Save Channel Details'); + channel.content.created_at = channel.content.created_at || channel?.nostrEvent.created_at || getNowSeconds(); const ev = await this.wallet.documentsIndex.createEvent(channel, privateKey); await this.dataService.publishEvent(ev); return true; @@ -400,7 +400,7 @@ export class Nip76Service { } const event1 = await channel.dkxRsvp.createEvent(rsvp, privateKey); await this.dataService.publishEvent(event1); - + rsvp.docIndex = channel.invitation.docIndex || (this.wallet.rsvps.length + 1 + walletRsvpDocumentsOffset); rsvp.content.signingKey = channel.invitation.pointer.signingKey; rsvp.content.cryptoKey = channel.invitation.pointer.cryptoKey; From 9de1dc574a3280471909cf6b2366a54c224d985b Mon Sep 17 00:00:00 2001 From: d-krause Date: Sun, 2 Apr 2023 21:20:49 -0400 Subject: [PATCH 25/39] rename component part 1 --- src/app/app-routing.module.ts | 2 +- src/app/app.module.ts | 2 +- .../nip76-settings.component.html | 0 .../nip76-settings.component.scss | 0 .../nip76-settings.component.spec.ts | 0 .../{nip76-settings => nip76-main}/nip76-settings.component.ts | 0 6 files changed, 2 insertions(+), 2 deletions(-) rename src/app/nip76/{nip76-settings => nip76-main}/nip76-settings.component.html (100%) rename src/app/nip76/{nip76-settings => nip76-main}/nip76-settings.component.scss (100%) rename src/app/nip76/{nip76-settings => nip76-main}/nip76-settings.component.spec.ts (100%) rename src/app/nip76/{nip76-settings => nip76-main}/nip76-settings.component.ts (100%) diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 46d23c41..161281a8 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -33,7 +33,7 @@ import { LoginComponent } from './connect/login/login'; import { CreateProfileComponent } from './connect/create/create'; import { EditorBadgesComponent } from './editor-badges/editor'; import { BadgeComponent } from './badge/badge'; -import { Nip76MainComponent } from './nip76/nip76-settings/nip76-settings.component'; +import { Nip76MainComponent } from './nip76/nip76-main/nip76-settings.component'; const routes: Routes = [ { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0c01eecc..2ae79a64 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -153,7 +153,7 @@ import { TagsComponent } from './shared/tags/tags'; import { BadgeComponent } from './badge/badge'; import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; import { DragScrollModule } from 'ngx-drag-scroll'; -import { Nip76MainComponent } from './nip76/nip76-settings/nip76-settings.component'; +import { Nip76MainComponent } from './nip76/nip76-main/nip76-settings.component'; import { AddChannelDialog } from './nip76/nip76-add-channel-dialog/add-channel-dialog.component'; import { Nip76EventButtonsComponent } from './nip76/nip76-event-buttons/nip76-event-buttons.component'; import { Nip76EventThreadComponent } from './nip76/nip76-event-thread/nip76-event-thread.component'; diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.html b/src/app/nip76/nip76-main/nip76-settings.component.html similarity index 100% rename from src/app/nip76/nip76-settings/nip76-settings.component.html rename to src/app/nip76/nip76-main/nip76-settings.component.html diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.scss b/src/app/nip76/nip76-main/nip76-settings.component.scss similarity index 100% rename from src/app/nip76/nip76-settings/nip76-settings.component.scss rename to src/app/nip76/nip76-main/nip76-settings.component.scss diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.spec.ts b/src/app/nip76/nip76-main/nip76-settings.component.spec.ts similarity index 100% rename from src/app/nip76/nip76-settings/nip76-settings.component.spec.ts rename to src/app/nip76/nip76-main/nip76-settings.component.spec.ts diff --git a/src/app/nip76/nip76-settings/nip76-settings.component.ts b/src/app/nip76/nip76-main/nip76-settings.component.ts similarity index 100% rename from src/app/nip76/nip76-settings/nip76-settings.component.ts rename to src/app/nip76/nip76-main/nip76-settings.component.ts From 2820cd09b73b11923bdad7777075824f42ceff72 Mon Sep 17 00:00:00 2001 From: d-krause Date: Sun, 2 Apr 2023 21:22:10 -0400 Subject: [PATCH 26/39] rename component part 2 --- src/app/app-routing.module.ts | 2 +- src/app/app.module.ts | 2 +- ...{nip76-settings.component.html => nip76-main.component.html} | 0 ...{nip76-settings.component.scss => nip76-main.component.scss} | 0 ...-settings.component.spec.ts => nip76-main.component.spec.ts} | 2 +- .../{nip76-settings.component.ts => nip76-main.component.ts} | 0 6 files changed, 3 insertions(+), 3 deletions(-) rename src/app/nip76/nip76-main/{nip76-settings.component.html => nip76-main.component.html} (100%) rename src/app/nip76/nip76-main/{nip76-settings.component.scss => nip76-main.component.scss} (100%) rename src/app/nip76/nip76-main/{nip76-settings.component.spec.ts => nip76-main.component.spec.ts} (89%) rename src/app/nip76/nip76-main/{nip76-settings.component.ts => nip76-main.component.ts} (100%) diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 161281a8..d0094947 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -33,7 +33,7 @@ import { LoginComponent } from './connect/login/login'; import { CreateProfileComponent } from './connect/create/create'; import { EditorBadgesComponent } from './editor-badges/editor'; import { BadgeComponent } from './badge/badge'; -import { Nip76MainComponent } from './nip76/nip76-main/nip76-settings.component'; +import { Nip76MainComponent } from './nip76/nip76-main/nip76-main.component'; const routes: Routes = [ { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 2ae79a64..8aa1cf22 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -153,7 +153,7 @@ import { TagsComponent } from './shared/tags/tags'; import { BadgeComponent } from './badge/badge'; import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; import { DragScrollModule } from 'ngx-drag-scroll'; -import { Nip76MainComponent } from './nip76/nip76-main/nip76-settings.component'; +import { Nip76MainComponent } from './nip76/nip76-main/nip76-main.component'; import { AddChannelDialog } from './nip76/nip76-add-channel-dialog/add-channel-dialog.component'; import { Nip76EventButtonsComponent } from './nip76/nip76-event-buttons/nip76-event-buttons.component'; import { Nip76EventThreadComponent } from './nip76/nip76-event-thread/nip76-event-thread.component'; diff --git a/src/app/nip76/nip76-main/nip76-settings.component.html b/src/app/nip76/nip76-main/nip76-main.component.html similarity index 100% rename from src/app/nip76/nip76-main/nip76-settings.component.html rename to src/app/nip76/nip76-main/nip76-main.component.html diff --git a/src/app/nip76/nip76-main/nip76-settings.component.scss b/src/app/nip76/nip76-main/nip76-main.component.scss similarity index 100% rename from src/app/nip76/nip76-main/nip76-settings.component.scss rename to src/app/nip76/nip76-main/nip76-main.component.scss diff --git a/src/app/nip76/nip76-main/nip76-settings.component.spec.ts b/src/app/nip76/nip76-main/nip76-main.component.spec.ts similarity index 89% rename from src/app/nip76/nip76-main/nip76-settings.component.spec.ts rename to src/app/nip76/nip76-main/nip76-main.component.spec.ts index 79c95d94..fa65f180 100644 --- a/src/app/nip76/nip76-main/nip76-settings.component.spec.ts +++ b/src/app/nip76/nip76-main/nip76-main.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Nip76MainComponent } from './nip76-settings.component'; +import { Nip76MainComponent } from './nip76-main.component'; describe('PrivateThreadsComponent', () => { let component: Nip76MainComponent; diff --git a/src/app/nip76/nip76-main/nip76-settings.component.ts b/src/app/nip76/nip76-main/nip76-main.component.ts similarity index 100% rename from src/app/nip76/nip76-main/nip76-settings.component.ts rename to src/app/nip76/nip76-main/nip76-main.component.ts From ad1297c2546959976da8972d49fe4df77177938c Mon Sep 17 00:00:00 2001 From: d-krause Date: Sun, 2 Apr 2023 21:22:56 -0400 Subject: [PATCH 27/39] rename component part 3 --- src/app/nip76/nip76-main/nip76-main.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/nip76/nip76-main/nip76-main.component.ts b/src/app/nip76/nip76-main/nip76-main.component.ts index 4669c23d..8c49fc8f 100644 --- a/src/app/nip76/nip76-main/nip76-main.component.ts +++ b/src/app/nip76/nip76-main/nip76-main.component.ts @@ -15,8 +15,8 @@ enum DisplayType { } @Component({ selector: 'app-nip76', - templateUrl: './nip76-settings.component.html', - styleUrls: ['./nip76-settings.component.scss'] + templateUrl: './nip76-main.component.html', + styleUrls: ['./nip76-main.component.scss'] }) export class Nip76MainComponent { From ad29b31d45a5848f2e06a1d8b5804234521be7ff Mon Sep 17 00:00:00 2001 From: d-krause Date: Tue, 4 Apr 2023 09:01:43 -0400 Subject: [PATCH 28/39] adding demo starter components --- src/app/app-routing.module.ts | 8 +- src/app/app.module.ts | 9 +- src/app/nip76/demo-only/auth-guard.ts | 25 +++ .../nip76-demo-starter.component.html | 18 ++ .../nip76-demo-starter.component.scss | 9 + .../nip76-demo-starter.component.spec.ts | 23 +++ .../nip76-demo-starter.component.ts | 183 ++++++++++++++++++ .../nip76/nip76-main/nip76-main.component.ts | 2 +- src/app/nip76/nip76.service.ts | 24 ++- 9 files changed, 284 insertions(+), 17 deletions(-) create mode 100644 src/app/nip76/demo-only/auth-guard.ts create mode 100644 src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.html create mode 100644 src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.scss create mode 100644 src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.spec.ts create mode 100644 src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index d0094947..3adefdb7 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -34,6 +34,8 @@ import { CreateProfileComponent } from './connect/create/create'; import { EditorBadgesComponent } from './editor-badges/editor'; import { BadgeComponent } from './badge/badge'; import { Nip76MainComponent } from './nip76/nip76-main/nip76-main.component'; +import { Nip76DemoAuthGuardService } from './nip76/demo-only/auth-guard'; +import { Nip76DemoStarterComponent } from './nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component'; const routes: Routes = [ { @@ -143,11 +145,15 @@ const routes: Routes = [ { path: 'private-channels', component: Nip76MainComponent, - canActivate: [AuthGuard], + canActivate: [Nip76DemoAuthGuardService], resolve: { data: LoadingResolverService, }, }, + { + path: 'private-channels/demo-setup', + component: Nip76DemoStarterComponent, + }, { path: 'private-channels/sent-rsvps', component: Nip76MainComponent, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 8aa1cf22..749c2c7a 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -163,6 +163,8 @@ import { Nip76DiagnosticsComponent } from './nip76/nip76-diagnostics/nip76-diagn import { Nip76ChannelHeaderComponent } from './nip76/nip76-channel/nip76-channel.component'; import { Nip76ChannelNotesComponent } from './nip76/nip76-channel-notes/nip76-channel-notes.component'; import { Nip76RsvpsSentComponent } from './nip76/nip76-rsvps-sent/nip76-rsvps-sent.component'; +import { Nip76DemoAuthGuardService } from './nip76/demo-only/auth-guard'; +import { Nip76DemoStarterComponent, Nip76DemoCreateComponent, Nip76DemoKeyComponent } from './nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component'; @NgModule({ declarations: [ @@ -263,7 +265,10 @@ import { Nip76RsvpsSentComponent } from './nip76/nip76-rsvps-sent/nip76-rsvps-se Nip76DiagnosticsComponent, Nip76ChannelHeaderComponent, Nip76ChannelNotesComponent, - Nip76RsvpsSentComponent + Nip76RsvpsSentComponent, + Nip76DemoKeyComponent, + Nip76DemoCreateComponent, + Nip76DemoStarterComponent ], imports: [ AboutModule, @@ -343,7 +348,7 @@ import { Nip76RsvpsSentComponent } from './nip76/nip76-rsvps-sent/nip76-rsvps-se }), ], exports: [], - providers: [{ provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' } }, AuthGuardService, AppUpdateService, CheckForUpdateService, ChatService, UserService], + providers: [{ provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' } }, AuthGuardService, AppUpdateService, CheckForUpdateService, ChatService, UserService, Nip76DemoAuthGuardService], bootstrap: [AppComponent], }) export class AppModule {} diff --git a/src/app/nip76/demo-only/auth-guard.ts b/src/app/nip76/demo-only/auth-guard.ts new file mode 100644 index 00000000..ef41f6b4 --- /dev/null +++ b/src/app/nip76/demo-only/auth-guard.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { Router, CanActivate } from '@angular/router'; +import { ApplicationState } from '../../services/applicationstate'; +import { AuthenticationService, UserInfo } from '../../services/authentication'; + +@Injectable() +export class Nip76DemoAuthGuardService implements CanActivate { + constructor(public appState: ApplicationState, private authService: AuthenticationService, public router: Router) { + localStorage.setItem('blockcore:notes:nostr:consent', 'true'); + } + canActivate() { + if (this.authService.authInfo$.getValue().authenticated()) { + return true; + } + + return this.authService.getAuthInfo().then((authInfo: UserInfo) => { + if (authInfo.authenticated()) { + return true; + } else { + this.router.navigateByUrl('/private-channels/demo-setup'); + return false; + } + }); + } +} \ No newline at end of file diff --git a/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.html b/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.html new file mode 100644 index 00000000..d6101960 --- /dev/null +++ b/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.html @@ -0,0 +1,18 @@ +
    + Thanks for coming to checkout the Nip76 Private Channels Demo. +

    + Feel free to use your own private nostr key, but we understand why many users would below hesitant + about doing this. +

    +

    + So feel free to use a demo profile like + Alice or Bob, + or create an entirely new user. +

    + + +
    + + \ No newline at end of file diff --git a/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.scss b/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.scss new file mode 100644 index 00000000..4ad4f558 --- /dev/null +++ b/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.scss @@ -0,0 +1,9 @@ +.intro { + position: absolute; + top: 130px; + left: 245px; + z-index: 1000; + width: 697px; + font-weight: bold; + background-color: white; +} \ No newline at end of file diff --git a/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.spec.ts b/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.spec.ts new file mode 100644 index 00000000..abb8fc91 --- /dev/null +++ b/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Nip76DemoStarterComponent } from './nip76-demo-starter.component'; + +describe('LoginOrCreateNewComponent', () => { + let component: Nip76DemoStarterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ Nip76DemoStarterComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(Nip76DemoStarterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.ts b/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.ts new file mode 100644 index 00000000..9e5b0338 --- /dev/null +++ b/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.ts @@ -0,0 +1,183 @@ +import { Component } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { Router } from '@angular/router'; +import { Nip76WebWalletStorage } from 'animiq-nip76-tools'; +import { Kind, getEventHash, validateEvent, signEvent, Event } from 'nostr-tools'; +import { CreateProfileComponent } from 'src/app/connect/create/create'; +import { ConnectKeyComponent } from 'src/app/connect/key/key'; +import { AuthenticationService } from 'src/app/services/authentication'; +import { DataService } from 'src/app/services/data'; +import { ProfileService } from 'src/app/services/profile'; +import { SecurityService } from 'src/app/services/security'; +import { ThemeService } from 'src/app/services/theme'; +import { Utilities } from 'src/app/services/utilities'; +import { defaultSnackBarOpts, Nip76Service } from '../../nip76.service'; + + + +@Component({ + selector: 'app-nip76-demo-starter', + templateUrl: './nip76-demo-starter.component.html', + styleUrls: ['./nip76-demo-starter.component.scss'] +}) +export class Nip76DemoStarterComponent { + demoUserType: 'existing' | 'new' = 'existing'; + constructor(private snackBar: MatSnackBar) { + + } + copyKey(name: 'Alice' | 'Bob') { + const key = { + 'Alice': 'nsec1y72ekupwshrl6zca2kx439uz23x4fqppc6gg9y9e5up5es06qqxqlcw698', + 'Bob': 'nsec12l6c5g8e7gt9twyctk0t073trlrf2zzs88240k3d2dmqlyh2hwhq9s2wl3' + }[name]; + navigator.clipboard.writeText(key); + this.snackBar.open(`${name}'s key is now in your clipboard.`, 'Hide', defaultSnackBarOpts); + } +} + +@Component({ + selector: 'nip76-demo-key', + templateUrl: '../../../connect/key/key.html', + styleUrls: ['../../../connect/key/key.css', '../../../connect/connect.css'] +}) +export class Nip76DemoKeyComponent extends ConnectKeyComponent { + + constructor( + dialog: MatDialog, + theme: ThemeService, + private router1: Router, + private security1: SecurityService, + private nip76Service: Nip76Service + ) { + super(dialog, theme, router1, security1); + this.step = 3; + } + + /** + * same as base class persistKey but with Nip76 wallet creation and redirection to private-channels + */ + override async persistKey() { + + setTimeout(async () => { + if (!this.privateKeyHex) { + return; + } + + if (!this.publicKeyHex) { + return; + } + + // First attempt to get public key from the private key to see if it's possible: + const encrypted = await this.security1.encryptData(this.privateKeyHex, this.password); + const decrypted = await this.security1.decryptData(encrypted, this.password); + + if (this.privateKeyHex == decrypted) { + localStorage.setItem('blockcore:notes:nostr:prvkey', encrypted); + localStorage.setItem('blockcore:notes:nostr:pubkey', this.publicKeyHex); + this.nip76Service.wallet = await Nip76WebWalletStorage.fromStorage({ + publicKey: this.publicKeyHex, + privateKey: this.privateKeyHex + }); + this.nip76Service.wallet.saveWallet(this.privateKeyHex); + + this.reset(); + + this.router1.navigateByUrl('/private-channels'); + // window.location.href = '/private-channels'; + } else { + this.error = 'Unable to encrypt and decrypt. Cannot continue.'; + console.error(this.error); + } + + // this.hidePrivateKey = false; + }, 10); + } + +} + +@Component({ + selector: 'nip76-demo-create', + templateUrl: '../../../connect/create/create.html', + styleUrls: ['../../../connect/create/create.css', '../../../connect/connect.css'] +}) +export class Nip76DemoCreateComponent extends CreateProfileComponent { + + constructor( + private utilities1: Utilities, + private dataService1: DataService, + private profileService1: ProfileService, + authService: AuthenticationService, + theme: ThemeService, + private router1: Router, + private security1: SecurityService, + private nip76Service: Nip76Service + ) { + super(utilities1, dataService1, profileService1, authService, theme, router1, security1) + } + + /** + * same as base class persistKey but with Nip76 wallet creation and redirection to private-channels + */ + override async persistKey() { + setTimeout(async () => { + if (!this.privateKeyHex) { + return; + } + + if (!this.publicKeyHex) { + return; + } + + // First attempt to get public key from the private key to see if it's possible: + const encrypted = await this.security1.encryptData(this.privateKeyHex, this.password); + const decrypted = await this.security1.decryptData(encrypted, this.password); + + if (this.privateKeyHex == decrypted) { + localStorage.setItem('blockcore:notes:nostr:prvkey', encrypted); + localStorage.setItem('blockcore:notes:nostr:pubkey', this.publicKeyHex); + + this.nip76Service.wallet = await Nip76WebWalletStorage.fromStorage({ + publicKey: this.publicKeyHex, + privateKey: this.privateKeyHex + }); + this.nip76Service.wallet.saveWallet(this.privateKeyHex); + + this.profile.npub = this.publicKey; + this.profile.pubkey = this.publicKeyHex; + + // Create and sign the profile event. + const profileContent = this.utilities1.reduceProfile(this.profile!); + let unsignedEvent = this.dataService1.createEventWithPubkey(Kind.Metadata, JSON.stringify(profileContent), this.publicKeyHex); + let signedEvent = unsignedEvent as Event; + signedEvent.id = await getEventHash(unsignedEvent); + + if (!validateEvent(signedEvent)) { + this.error = 'Unable to validate the event. Cannot continue.'; + } + + const signature = signEvent(signedEvent, this.privateKeyHex) as any; + signedEvent.sig = signature; + + // Make sure we reset the secrets. + this.mnemonic = ''; + this.privateKey = ''; + this.privateKeyHex = ''; + this.publicKey = ''; + this.publicKeyHex = ''; + this.password = ''; + this.profile = null; + + this.profileService1.newProfileEvent = signedEvent; + + this.router1.navigateByUrl('/private-channels'); + } else { + this.error = 'Unable to encrypt and decrypt. Cannot continue.'; + console.error(this.error); + } + + // this.hidePrivateKey = false; + }, 10); + } +} + diff --git a/src/app/nip76/nip76-main/nip76-main.component.ts b/src/app/nip76/nip76-main/nip76-main.component.ts index 8c49fc8f..7f570688 100644 --- a/src/app/nip76/nip76-main/nip76-main.component.ts +++ b/src/app/nip76/nip76-main/nip76-main.component.ts @@ -75,7 +75,7 @@ export class Nip76MainComponent { } randomizeKey() { - this.wallet.reKey(); + // this.wallet.reKey(); this.editChannel = this.wallet.channels[0]; this.editChannel.editing = true; this.editChannel.content.name = 'Example Channel 1'; diff --git a/src/app/nip76/nip76.service.ts b/src/app/nip76/nip76.service.ts index c079e8d6..683201f2 100644 --- a/src/app/nip76/nip76.service.ts +++ b/src/app/nip76/nip76.service.ts @@ -8,7 +8,7 @@ import { Nip76WebWalletStorage, NostrKinds, PostDocument, PrivateChannel, Rsvp, Versions, walletRsvpDocumentsOffset } from 'animiq-nip76-tools'; import * as nostrTools from 'nostr-tools'; -import { firstValueFrom, Subject } from 'rxjs'; +import { filter, firstValueFrom, Subject } from 'rxjs'; import { DataService } from '../services/data'; import { NostrEvent, NostrRelaySubscription } from '../services/interfaces'; import { ProfileService } from '../services/profile'; @@ -48,16 +48,16 @@ export class Nip76Service { private dataService: DataService, private ui: UIService ) { - Nip76WebWalletStorage.fromStorage({ publicKey: this.profileService.profile!.pubkey }).then(wallet => { - this.wallet = wallet; - if (this.wallet.isInSession) { - // Array(4).forEach((_, i) => wallet.getChannel(i)); - // this.loadChannel(wallet.channels[0]); - this.loadDocuments(); - } else if (!this.wallet.isGuest) { - this.login(); - } - }); + if (this.profileService.profile) { + Nip76WebWalletStorage.fromStorage({ publicKey: this.profileService.profile!.pubkey }).then(wallet => { + this.wallet = wallet; + if (this.wallet.isInSession) { + this.loadDocuments(); + } else if (!this.wallet.isGuest) { + this.login(); + } + }); + } } async passwordDialog(actionPrompt: string): Promise { @@ -94,8 +94,6 @@ export class Nip76Service { const privateKey = await this.passwordDialog('Load Private Channel Keys'); this.wallet = await Nip76WebWalletStorage.fromStorage({ privateKey }); if (this.wallet.isInSession) { - // Array(4).forEach((_, i) => this.wallet.getChannel(i)); - // this.loadChannel(this.wallet.channels[0]); this.loadDocuments(); } return this.wallet.isInSession; From 7a023f8b32c8689cee4fe20e8b1c79702a2f8281 Mon Sep 17 00:00:00 2001 From: d-krause Date: Tue, 4 Apr 2023 15:00:34 -0400 Subject: [PATCH 29/39] fully functionining demo code --- src/app/app-routing.module.ts | 4 +- src/app/app.module.ts | 4 +- src/app/connect/create/create.ts | 13 +- src/app/connect/key/key.ts | 11 +- src/app/nip76/demo-only/auth-guard.ts | 25 --- .../nip76-demo-starter.component.html | 26 ++-- .../nip76-demo-starter.component.scss | 29 +++- .../nip76-demo-starter.component.ts | 144 ++++-------------- src/app/nip76/demo-only/nip76-demo.service.ts | 44 ++++++ .../nip76-channel-notes.component.ts | 2 +- .../nip76-channel.component.html | 2 +- .../nip76-channel/nip76-channel.component.ts | 18 ++- .../nip76-main/nip76-main.component.html | 85 +++++------ .../nip76-main/nip76-main.component.scss | 10 ++ .../nip76/nip76-main/nip76-main.component.ts | 38 ++--- src/app/nip76/nip76.service.ts | 14 +- tsconfig.json | 3 - 17 files changed, 228 insertions(+), 244 deletions(-) delete mode 100644 src/app/nip76/demo-only/auth-guard.ts create mode 100644 src/app/nip76/demo-only/nip76-demo.service.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 3adefdb7..0a344bd1 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -34,7 +34,7 @@ import { CreateProfileComponent } from './connect/create/create'; import { EditorBadgesComponent } from './editor-badges/editor'; import { BadgeComponent } from './badge/badge'; import { Nip76MainComponent } from './nip76/nip76-main/nip76-main.component'; -import { Nip76DemoAuthGuardService } from './nip76/demo-only/auth-guard'; +import { Nip76DemoService } from './nip76/demo-only/nip76-demo.service'; import { Nip76DemoStarterComponent } from './nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component'; const routes: Routes = [ @@ -145,7 +145,7 @@ const routes: Routes = [ { path: 'private-channels', component: Nip76MainComponent, - canActivate: [Nip76DemoAuthGuardService], + canActivate: [Nip76DemoService], resolve: { data: LoadingResolverService, }, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 749c2c7a..2bd27a83 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -163,7 +163,7 @@ import { Nip76DiagnosticsComponent } from './nip76/nip76-diagnostics/nip76-diagn import { Nip76ChannelHeaderComponent } from './nip76/nip76-channel/nip76-channel.component'; import { Nip76ChannelNotesComponent } from './nip76/nip76-channel-notes/nip76-channel-notes.component'; import { Nip76RsvpsSentComponent } from './nip76/nip76-rsvps-sent/nip76-rsvps-sent.component'; -import { Nip76DemoAuthGuardService } from './nip76/demo-only/auth-guard'; +import { Nip76DemoService } from './nip76/demo-only/nip76-demo.service'; import { Nip76DemoStarterComponent, Nip76DemoCreateComponent, Nip76DemoKeyComponent } from './nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component'; @NgModule({ @@ -348,7 +348,7 @@ import { Nip76DemoStarterComponent, Nip76DemoCreateComponent, Nip76DemoKeyCompon }), ], exports: [], - providers: [{ provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' } }, AuthGuardService, AppUpdateService, CheckForUpdateService, ChatService, UserService, Nip76DemoAuthGuardService], + providers: [{ provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' } }, AuthGuardService, AppUpdateService, CheckForUpdateService, ChatService, UserService, Nip76DemoService], bootstrap: [AppComponent], }) export class AppModule {} diff --git a/src/app/connect/create/create.ts b/src/app/connect/create/create.ts index f449e5ff..c219d337 100644 --- a/src/app/connect/create/create.ts +++ b/src/app/connect/create/create.ts @@ -7,6 +7,7 @@ import { ThemeService } from '../../services/theme'; import { ProfileService } from '../../services/profile'; import { Utilities } from 'src/app/services/utilities'; import { DataService } from 'src/app/services/data'; +import { Nip76DemoService } from 'src/app/nip76/demo-only/nip76-demo.service'; @Component({ selector: 'app-create', @@ -31,7 +32,8 @@ export class CreateProfileComponent { private authService: AuthenticationService, public theme: ThemeService, private router: Router, - private security: SecurityService + private security: SecurityService, + private nip76DemoService: Nip76DemoService ) {} ngOnInit() { @@ -65,16 +67,19 @@ export class CreateProfileComponent { if (!this.privateKeyHex) { return; } - + if (!this.publicKeyHex) { return; } - + // First attempt to get public key from the private key to see if it's possible: const encrypted = await this.security.encryptData(this.privateKeyHex, this.password); const decrypted = await this.security.decryptData(encrypted, this.password); - + if (this.privateKeyHex == decrypted) { + + await this.nip76DemoService.createNip76Wallet(this.publicKeyHex, this.privateKeyHex); + localStorage.setItem('blockcore:notes:nostr:prvkey', encrypted); localStorage.setItem('blockcore:notes:nostr:pubkey', this.publicKeyHex); diff --git a/src/app/connect/key/key.ts b/src/app/connect/key/key.ts index 29aae4bd..757d566f 100644 --- a/src/app/connect/key/key.ts +++ b/src/app/connect/key/key.ts @@ -3,6 +3,7 @@ import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { base64 } from '@scure/base'; import { relayInit, Relay, Event, utils, getPublicKey, nip19, nip06 } from 'nostr-tools'; +import { Nip76DemoService } from 'src/app/nip76/demo-only/nip76-demo.service'; import { SecurityService } from '../../services/security'; import { ThemeService } from '../../services/theme'; import { QrScanDialog } from './qr-scan-dialog/qr-scan'; @@ -22,7 +23,8 @@ export class ConnectKeyComponent { step = 1; mnemonic: string = ''; - constructor(public dialog: MatDialog, public theme: ThemeService, private router: Router, private security: SecurityService) {} + constructor(public dialog: MatDialog, public theme: ThemeService, private router: Router, private security: SecurityService, + private nip76DemoService: Nip76DemoService) {} setPrivateKey() { this.privateKeyHex = nip06.privateKeyFromSeedWords(this.mnemonic); @@ -43,7 +45,7 @@ export class ConnectKeyComponent { async persistKey() { // this.hidePrivateKey = true; - + setTimeout(async () => { if (!this.privateKeyHex) { return; @@ -56,8 +58,11 @@ export class ConnectKeyComponent { // First attempt to get public key from the private key to see if it's possible: const encrypted = await this.security.encryptData(this.privateKeyHex, this.password); const decrypted = await this.security.decryptData(encrypted, this.password); - + if (this.privateKeyHex == decrypted) { + + await this.nip76DemoService.createNip76Wallet(this.publicKeyHex, this.privateKeyHex); + localStorage.setItem('blockcore:notes:nostr:prvkey', encrypted); localStorage.setItem('blockcore:notes:nostr:pubkey', this.publicKeyHex); diff --git a/src/app/nip76/demo-only/auth-guard.ts b/src/app/nip76/demo-only/auth-guard.ts deleted file mode 100644 index ef41f6b4..00000000 --- a/src/app/nip76/demo-only/auth-guard.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Router, CanActivate } from '@angular/router'; -import { ApplicationState } from '../../services/applicationstate'; -import { AuthenticationService, UserInfo } from '../../services/authentication'; - -@Injectable() -export class Nip76DemoAuthGuardService implements CanActivate { - constructor(public appState: ApplicationState, private authService: AuthenticationService, public router: Router) { - localStorage.setItem('blockcore:notes:nostr:consent', 'true'); - } - canActivate() { - if (this.authService.authInfo$.getValue().authenticated()) { - return true; - } - - return this.authService.getAuthInfo().then((authInfo: UserInfo) => { - if (authInfo.authenticated()) { - return true; - } else { - this.router.navigateByUrl('/private-channels/demo-setup'); - return false; - } - }); - } -} \ No newline at end of file diff --git a/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.html b/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.html index d6101960..67c1a3bb 100644 --- a/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.html +++ b/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.html @@ -1,18 +1,18 @@ -
    - Thanks for coming to checkout the Nip76 Private Channels Demo. + + Thanks for coming to checkout the Nip76 Private Channels Demo.

    - Feel free to use your own private nostr key, but we understand why many users would below hesitant - about doing this. -

    -

    - So feel free to use a demo profile like - Alice or Bob, + You may use your own private nostr key, but we understand why many users would be hesitant + to do this. If so, please feel free to use a demo profile like + Alice + or Bob, or create an entirely new user.

    - - -
    + + \ No newline at end of file diff --git a/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.scss b/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.scss index 4ad4f558..78c1490c 100644 --- a/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.scss +++ b/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.scss @@ -1,9 +1,28 @@ .intro { position: absolute; - top: 130px; - left: 245px; + top: 86px; + padding: 8px; + left: 198px; z-index: 1000; - width: 697px; - font-weight: bold; - background-color: white; + width: 66%; + background-color: floralwhite; + border-radius: 20px; +} + +.menubar { + display: flex; + flex-direction: row; + justify-content: center; +} + +.menubar-button { + display: flex; +} + +.action-link { + cursor: pointer; +} + +p { + margin: 4px 0px; } \ No newline at end of file diff --git a/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.ts b/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.ts index 9e5b0338..9b79c8fe 100644 --- a/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.ts +++ b/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.ts @@ -2,8 +2,7 @@ import { Component } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; import { Router } from '@angular/router'; -import { Nip76WebWalletStorage } from 'animiq-nip76-tools'; -import { Kind, getEventHash, validateEvent, signEvent, Event } from 'nostr-tools'; +import { filter } from 'rxjs'; import { CreateProfileComponent } from 'src/app/connect/create/create'; import { ConnectKeyComponent } from 'src/app/connect/key/key'; import { AuthenticationService } from 'src/app/services/authentication'; @@ -12,7 +11,8 @@ import { ProfileService } from 'src/app/services/profile'; import { SecurityService } from 'src/app/services/security'; import { ThemeService } from 'src/app/services/theme'; import { Utilities } from 'src/app/services/utilities'; -import { defaultSnackBarOpts, Nip76Service } from '../../nip76.service'; +import { defaultSnackBarOpts } from '../../nip76.service'; +import { Nip76DemoService } from '../nip76-demo.service'; @@ -23,9 +23,18 @@ import { defaultSnackBarOpts, Nip76Service } from '../../nip76.service'; }) export class Nip76DemoStarterComponent { demoUserType: 'existing' | 'new' = 'existing'; - constructor(private snackBar: MatSnackBar) { - + constructor( + private snackBar: MatSnackBar, + private profileService: ProfileService, + private router: Router + ) { } + + ngOnInit() { + this.profileService.profile$.pipe(filter(x => !!x)).subscribe(x => { + this.router.navigateByUrl('/private-channels'); + }); } + copyKey(name: 'Alice' | 'Bob') { const key = { 'Alice': 'nsec1y72ekupwshrl6zca2kx439uz23x4fqppc6gg9y9e5up5es06qqxqlcw698', @@ -47,51 +56,19 @@ export class Nip76DemoKeyComponent extends ConnectKeyComponent { dialog: MatDialog, theme: ThemeService, private router1: Router, - private security1: SecurityService, - private nip76Service: Nip76Service + security: SecurityService, + nip76DemoService: Nip76DemoService + ) { - super(dialog, theme, router1, security1); + super(dialog, theme, router1, security, nip76DemoService); this.step = 3; } - /** - * same as base class persistKey but with Nip76 wallet creation and redirection to private-channels - */ override async persistKey() { - - setTimeout(async () => { - if (!this.privateKeyHex) { - return; - } - - if (!this.publicKeyHex) { - return; - } - - // First attempt to get public key from the private key to see if it's possible: - const encrypted = await this.security1.encryptData(this.privateKeyHex, this.password); - const decrypted = await this.security1.decryptData(encrypted, this.password); - - if (this.privateKeyHex == decrypted) { - localStorage.setItem('blockcore:notes:nostr:prvkey', encrypted); - localStorage.setItem('blockcore:notes:nostr:pubkey', this.publicKeyHex); - this.nip76Service.wallet = await Nip76WebWalletStorage.fromStorage({ - publicKey: this.publicKeyHex, - privateKey: this.privateKeyHex - }); - this.nip76Service.wallet.saveWallet(this.privateKeyHex); - - this.reset(); - - this.router1.navigateByUrl('/private-channels'); - // window.location.href = '/private-channels'; - } else { - this.error = 'Unable to encrypt and decrypt. Cannot continue.'; - console.error(this.error); - } - - // this.hidePrivateKey = false; - }, 10); + super.persistKey(); + setTimeout(() => { + this.router1.navigateByUrl('/private-channels'); + }, 200) } } @@ -104,80 +81,23 @@ export class Nip76DemoKeyComponent extends ConnectKeyComponent { export class Nip76DemoCreateComponent extends CreateProfileComponent { constructor( - private utilities1: Utilities, - private dataService1: DataService, - private profileService1: ProfileService, + utilities: Utilities, + dataService: DataService, + profileService: ProfileService, authService: AuthenticationService, theme: ThemeService, private router1: Router, - private security1: SecurityService, - private nip76Service: Nip76Service + security: SecurityService, + nip76DemoService: Nip76DemoService ) { - super(utilities1, dataService1, profileService1, authService, theme, router1, security1) + super(utilities, dataService, profileService, authService, theme, router1, security, nip76DemoService) } - /** - * same as base class persistKey but with Nip76 wallet creation and redirection to private-channels - */ override async persistKey() { - setTimeout(async () => { - if (!this.privateKeyHex) { - return; - } - - if (!this.publicKeyHex) { - return; - } - - // First attempt to get public key from the private key to see if it's possible: - const encrypted = await this.security1.encryptData(this.privateKeyHex, this.password); - const decrypted = await this.security1.decryptData(encrypted, this.password); - - if (this.privateKeyHex == decrypted) { - localStorage.setItem('blockcore:notes:nostr:prvkey', encrypted); - localStorage.setItem('blockcore:notes:nostr:pubkey', this.publicKeyHex); - - this.nip76Service.wallet = await Nip76WebWalletStorage.fromStorage({ - publicKey: this.publicKeyHex, - privateKey: this.privateKeyHex - }); - this.nip76Service.wallet.saveWallet(this.privateKeyHex); - - this.profile.npub = this.publicKey; - this.profile.pubkey = this.publicKeyHex; - - // Create and sign the profile event. - const profileContent = this.utilities1.reduceProfile(this.profile!); - let unsignedEvent = this.dataService1.createEventWithPubkey(Kind.Metadata, JSON.stringify(profileContent), this.publicKeyHex); - let signedEvent = unsignedEvent as Event; - signedEvent.id = await getEventHash(unsignedEvent); - - if (!validateEvent(signedEvent)) { - this.error = 'Unable to validate the event. Cannot continue.'; - } - - const signature = signEvent(signedEvent, this.privateKeyHex) as any; - signedEvent.sig = signature; - - // Make sure we reset the secrets. - this.mnemonic = ''; - this.privateKey = ''; - this.privateKeyHex = ''; - this.publicKey = ''; - this.publicKeyHex = ''; - this.password = ''; - this.profile = null; - - this.profileService1.newProfileEvent = signedEvent; - - this.router1.navigateByUrl('/private-channels'); - } else { - this.error = 'Unable to encrypt and decrypt. Cannot continue.'; - console.error(this.error); - } - - // this.hidePrivateKey = false; - }, 10); + super.persistKey(); + setTimeout(() => { + this.router1.navigateByUrl('/private-channels'); + }, 200) } } diff --git a/src/app/nip76/demo-only/nip76-demo.service.ts b/src/app/nip76/demo-only/nip76-demo.service.ts new file mode 100644 index 00000000..05f25e71 --- /dev/null +++ b/src/app/nip76/demo-only/nip76-demo.service.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@angular/core'; +import { Router, CanActivate } from '@angular/router'; +import { Nip76WebWalletStorage } from 'animiq-nip76-tools'; +import { ApplicationState } from '../../services/applicationstate'; +import { AuthenticationService, UserInfo } from '../../services/authentication'; +import { Nip76Service } from '../nip76.service'; + +@Injectable() +export class Nip76DemoService implements CanActivate { + constructor( + public appState: ApplicationState, + private authService: AuthenticationService, + public router: Router, + private nip76Service: Nip76Service + ) { + localStorage.setItem('blockcore:notes:nostr:consent', 'true'); + } + canActivate() { + if (this.authService.authInfo$.getValue().authenticated()) { + return true; + } + return this.authService.getAuthInfo().then((authInfo: UserInfo) => { + if (authInfo.authenticated()) { + return true; + } else { + this.router.navigateByUrl('/private-channels/demo-setup'); + return false; + } + }); + } + + async createNip76Wallet(publicKey: string, privateKey: string) { + + // these Alice and Bob accounts are used for nip76 demostration only. they would not be needed in a final build + if (publicKey === '6ea813a435667275c736d722261dc2516c14452c421342a1e6a42046d849c8b3') { //Alice + localStorage.setItem(Nip76WebWalletStorage.backupKey, 'cec4LbCMhRCugybUHTqsZh97hzl6+eABGghHTWs/xO1Xyrx8KnDHQOc8yclbA35/Uz8TBqb5UohPRXZIeFRJj0AxxUw3Fpv2LcHA2gvyBIKpiwGdMIcq8fCzbFdEN7tb'); + } else if (publicKey === 'c94f40831616c246675a134f457e2a18db19570159e920dd62f91b66635982e1') { //Bob + localStorage.setItem(Nip76WebWalletStorage.backupKey, 'wJAM+65IkkjXd3EyvITdSwv+o8NvAgB6NP+lM3JE5qoWoYjkod08XpTu4q5oRIZfYz4FQj4QEJo2Q6zKcOVPWbmtVAxsoQh6Jbj4KxV3+C9fClqY1RhDAYGJyocWiKYW'); + } + + this.nip76Service.wallet = await Nip76WebWalletStorage.fromStorage({ publicKey, privateKey }); + this.nip76Service.wallet.saveWallet(privateKey); + } +} \ No newline at end of file diff --git a/src/app/nip76/nip76-channel-notes/nip76-channel-notes.component.ts b/src/app/nip76/nip76-channel-notes/nip76-channel-notes.component.ts index 31f98908..b194f093 100644 --- a/src/app/nip76/nip76-channel-notes/nip76-channel-notes.component.ts +++ b/src/app/nip76/nip76-channel-notes/nip76-channel-notes.component.ts @@ -61,7 +61,7 @@ export class Nip76ChannelNotesComponent { } shouldRSVP(channel: PrivateChannel) { - if (channel.ownerPubKey === this.wallet.ownerPubKey) return false; + if (channel.dkxPost.signingParent.privateKey) return false; if (channel.invitation?.pointer?.docIndex !== undefined) { const huh = this.wallet.rsvps.filter(x => x.content.pointerDocIndex === channel.invitation?.pointer.docIndex); return huh.length === 0; diff --git a/src/app/nip76/nip76-channel/nip76-channel.component.html b/src/app/nip76/nip76-channel/nip76-channel.component.html index 47d7c455..a100a818 100644 --- a/src/app/nip76/nip76-channel/nip76-channel.component.html +++ b/src/app/nip76/nip76-channel/nip76-channel.component.html @@ -41,7 +41,7 @@ -
    diff --git a/src/app/nip76/nip76-channel/nip76-channel.component.ts b/src/app/nip76/nip76-channel/nip76-channel.component.ts index 82886ef2..c1575d43 100644 --- a/src/app/nip76/nip76-channel/nip76-channel.component.ts +++ b/src/app/nip76/nip76-channel/nip76-channel.component.ts @@ -1,5 +1,6 @@ import { Component, Input } from '@angular/core'; import { MatSnackBar } from '@angular/material/snack-bar'; +import { Router } from '@angular/router'; import { ContentDocument, Invitation, Nip76Wallet, PostDocument, PrivateChannel, Rsvp } from 'animiq-nip76-tools'; import { CircleService } from 'src/app/services/circle'; import { Circle, NostrProfileDocument } from 'src/app/services/interfaces'; @@ -37,7 +38,8 @@ export class Nip76ChannelHeaderComponent { private profiles: ProfileService, private circleService: CircleService, private snackBar: MatSnackBar, - public nip76Service: Nip76Service + public nip76Service: Nip76Service, + private router: Router ) { } @Input() @@ -74,7 +76,7 @@ export class Nip76ChannelHeaderComponent { } get isOwner(): boolean { - return this.channel.ownerPubKey === this.wallet.ownerPubKey; + return !!this.channel.dkxPost.signingParent.privateKey; } async updateProfileDetails() { @@ -85,6 +87,10 @@ export class Nip76ChannelHeaderComponent { } } + viewNotes() { + this.router.navigate(['/private-channels', this.pubkey, 'notes']) + } + async saveChannel() { const success = await this.nip76Service.saveChannel(this.channel!); } @@ -102,12 +108,12 @@ export class Nip76ChannelHeaderComponent { async deleteRSVP(rsvp: Rsvp) { const privateKey = await this.nip76Service.passwordDialog('Delete RSVP'); if (privateKey) { - const channelRsvp = rsvp.channel?.rsvps.find(x => x.content.pubkey === this.wallet.ownerPubKey && x.content.pointerDocIndex === rsvp.content.pointerDocIndex); + const walletRsvp = this.wallet.rsvps.find(x => x.content.pubkey === this.wallet.ownerPubKey && x.content.pointerDocIndex === rsvp.content.pointerDocIndex); if (await this.nip76Service.deleteDocument(rsvp, privateKey)) { rsvp.dkxParent.documents.splice(rsvp.dkxParent.documents.indexOf(rsvp), 1); - if (channelRsvp) { - if (await this.nip76Service.deleteDocument(channelRsvp, privateKey)) { - channelRsvp.dkxParent.documents.splice(channelRsvp.dkxParent.documents.indexOf(channelRsvp), 1); + if (walletRsvp) { + if (await this.nip76Service.deleteDocument(walletRsvp, privateKey)) { + walletRsvp.dkxParent.documents.splice(walletRsvp.dkxParent.documents.indexOf(walletRsvp), 1); } } } diff --git a/src/app/nip76/nip76-main/nip76-main.component.html b/src/app/nip76/nip76-main/nip76-main.component.html index 34618b46..811144fb 100644 --- a/src/app/nip76/nip76-main/nip76-main.component.html +++ b/src/app/nip76/nip76-main/nip76-main.component.html @@ -4,63 +4,62 @@ mat-flat-button color="primary" (click)="listChannels()" style="display: block;"> < Channel List + +
    - - - - + (click)="nip76Service.login()"> + Start Secure Session + + (click)="nip76Service.logout()"> + End Secure Session + +
    -
    +

    NIP 76 Private Channels are not initialized for this profile yet.

    -

    To get started:

    -
      -
    1. - Randomize the keys. (OPTIONAL). -
    2. -
    3. - Save it to begin using. -
    4. -
    -
    - - Index - - - - Name - - - - About - - -
    - - -
    - - -
    -
    -
    +
    + + Thanks for coming to checkout the Nip76 Private Channels Demo. +

    + You have no channels yet. You can create one of your own and invite others + or read an invitation to another's channel. +

    +

    + Here is an invitation + from a Demo user named Alice. + The password is "test". +

    +
    + + +
    + + +
    +
    +
    +
    diff --git a/src/app/nip76/nip76-main/nip76-main.component.scss b/src/app/nip76/nip76-main/nip76-main.component.scss index 4d27c80c..05d94948 100644 --- a/src/app/nip76/nip76-main/nip76-main.component.scss +++ b/src/app/nip76/nip76-main/nip76-main.component.scss @@ -94,3 +94,13 @@ li.instruction { flex-wrap: nowrap; justify-content: flex-end; } + +.intro { + padding: 8px; + background-color: floralwhite; + border-radius: 20px; +} + +p { + margin: 4px 0px; +} \ No newline at end of file diff --git a/src/app/nip76/nip76-main/nip76-main.component.ts b/src/app/nip76/nip76-main/nip76-main.component.ts index 7f570688..b3926388 100644 --- a/src/app/nip76/nip76-main/nip76-main.component.ts +++ b/src/app/nip76/nip76-main/nip76-main.component.ts @@ -1,11 +1,11 @@ import { Component } from '@angular/core'; import { MatSnackBar } from '@angular/material/snack-bar'; import { ActivatedRoute, Router } from '@angular/router'; -import { Nip76Wallet, PrivateChannel } from 'animiq-nip76-tools'; +import { Nip76Wallet, Nip76WebWalletStorage, PrivateChannel } from 'animiq-nip76-tools'; import { ApplicationState } from '../../services/applicationstate'; import { NavigationService } from '../../services/navigation'; import { UIService } from '../../services/ui'; -import { Nip76Service } from '../nip76.service'; +import { defaultSnackBarOpts, Nip76Service } from '../nip76.service'; enum DisplayType { GuestUser = 'GuestUser', @@ -21,9 +21,8 @@ enum DisplayType { export class Nip76MainComponent { DisplayType = DisplayType; - editChannel?: PrivateChannel; activeChannelId!: string | null; - + showHelp = false; constructor( private router: Router, private activatedRoute: ActivatedRoute, @@ -38,6 +37,9 @@ export class Nip76MainComponent { this.activatedRoute.paramMap.subscribe(async (params) => { this.activeChannelId = params.get('channelPubKey'); }); + setTimeout(() => { + this.showHelp = this.wallet?.isInSession && this.wallet?.channels?.length === 0; + }, 3000); } get displayType(): DisplayType { @@ -48,8 +50,8 @@ export class Nip76MainComponent { const channel = this.nip76Service.findChannel(this.activeChannelId); if (channel) { return DisplayType.SingleChannel; - } - } if( this.router.url.endsWith('/sent-rsvps')) { + } + } if (this.router.url.endsWith('/sent-rsvps')) { return DisplayType.SentRSVPs; } else { return DisplayType.ChannelList; @@ -74,19 +76,20 @@ export class Nip76MainComponent { return undefined; } - randomizeKey() { - // this.wallet.reKey(); - this.editChannel = this.wallet.channels[0]; - this.editChannel.editing = true; - this.editChannel.content.name = 'Example Channel 1'; - this.editChannel.content.about = 'My First Private Channel 1'; - this.editChannel.ready = true; + async initPrivateChannels() { + const publicKey = this.nip76Service.profile.pubkey; + const privateKey = await this.nip76Service.passwordDialog('Create an HD Wallet'); + this.nip76Service.wallet = await Nip76WebWalletStorage.fromStorage({ publicKey, privateKey }); + this.nip76Service.wallet.saveWallet(privateKey); + location.reload(); } - async saveConfiguration() { - const savedLocal = await this.nip76Service.saveWallet(); - const savedRemote = await this.nip76Service.saveChannel(this.editChannel!); - location.reload(); + copyDemoInvitation(name: 'Alice') { + const invitation = { + 'Alice': 'nprivatechan1z5ay69wdt282c54z0m5rnmvqmr5elgsadczlkn5j50d7r2mtll8klfcxm76rzg90eqjdep70c88wur2sgvw0qt90vt3jw9lfser66hkcwywjxqudzfws20zyex2pktzmfjk0hpdehu9d4swanmcsckayfxrr0wgyvzm0j6' + }[name]; + navigator.clipboard.writeText(invitation); + this.snackBar.open(`The invitation is now in your clipboard. Click Read Invitation and paste it there.`, 'Hide', defaultSnackBarOpts); } async readInvitation() { @@ -100,7 +103,6 @@ export class Nip76MainComponent { createChannel() { let newChannel = this.wallet.createChannel(); newChannel.ready = newChannel.editing = true; - this.editChannel = newChannel; this.router.navigate(['/private-channels']); } diff --git a/src/app/nip76/nip76.service.ts b/src/app/nip76/nip76.service.ts index 683201f2..4aadafc9 100644 --- a/src/app/nip76/nip76.service.ts +++ b/src/app/nip76/nip76.service.ts @@ -8,9 +8,9 @@ import { Nip76WebWalletStorage, NostrKinds, PostDocument, PrivateChannel, Rsvp, Versions, walletRsvpDocumentsOffset } from 'animiq-nip76-tools'; import * as nostrTools from 'nostr-tools'; -import { filter, firstValueFrom, Subject } from 'rxjs'; +import { filter, firstValueFrom, Subject, take } from 'rxjs'; import { DataService } from '../services/data'; -import { NostrEvent, NostrRelaySubscription } from '../services/interfaces'; +import { NostrEvent, NostrProfileDocument, NostrRelaySubscription } from '../services/interfaces'; import { ProfileService } from '../services/profile'; import { RelayService } from '../services/relay'; import { SecurityService } from '../services/security'; @@ -38,6 +38,7 @@ export class Nip76Service { wallet!: Nip76Wallet; documentsSubscription?: NostrRelaySubscription; + profile!: NostrProfileDocument; constructor( private dialog: MatDialog, @@ -48,16 +49,17 @@ export class Nip76Service { private dataService: DataService, private ui: UIService ) { - if (this.profileService.profile) { - Nip76WebWalletStorage.fromStorage({ publicKey: this.profileService.profile!.pubkey }).then(wallet => { + this.profileService.profile$.pipe(filter(x => !!x), take(1)).subscribe(profile => { + this.profile = profile!; + Nip76WebWalletStorage.fromStorage({ publicKey: profile!.pubkey }).then(wallet => { this.wallet = wallet; if (this.wallet.isInSession) { - this.loadDocuments(); + setTimeout(() => { this.loadDocuments(); }, 500); } else if (!this.wallet.isGuest) { this.login(); } }); - } + }); } async passwordDialog(actionPrompt: string): Promise { diff --git a/tsconfig.json b/tsconfig.json index 6c40477f..15efd15f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,9 +25,6 @@ "dom" ], "paths": { - "animiq-lib": [ - "../../animiq-lib/dist/animiq-lib" - ], "animiq-nip76-tools": [ "../../animiq-nip76-tools/dist/" ], From 037c7d663d85c4ae977d5ec6ff685408124d23d9 Mon Sep 17 00:00:00 2001 From: d-krause Date: Thu, 6 Apr 2023 10:22:42 -0400 Subject: [PATCH 30/39] new dependency for event-buttons component --- .../nip76-event-buttons/nip76-event-buttons.component.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/nip76/nip76-event-buttons/nip76-event-buttons.component.ts b/src/app/nip76/nip76-event-buttons/nip76-event-buttons.component.ts index 2bd25191..f128d0cc 100644 --- a/src/app/nip76/nip76-event-buttons/nip76-event-buttons.component.ts +++ b/src/app/nip76/nip76-event-buttons/nip76-event-buttons.component.ts @@ -11,6 +11,7 @@ import { Utilities } from 'src/app/services/utilities'; import { PostDocument } from '../../../../../../animiq-nip76-tools/dist/src'; import { EventButtonsComponent } from '../../shared/event-buttons/event-buttons'; import { Nip76Service } from '../nip76.service'; +import { NotesService } from 'src/app/services/notes'; @Component({ selector: 'app-nip76-event-buttons', templateUrl: '../../shared/event-buttons/event-buttons.html', @@ -29,12 +30,13 @@ export class Nip76EventButtonsComponent extends EventButtonsComponent { constructor( private nip76Service: Nip76Service, eventService: EventService, + notesService: NotesService, dataService: DataService, optionsService: OptionsService, profileService: ProfileService, utilities: Utilities, dialog: MatDialog) { - super(eventService, dataService, optionsService, profileService, utilities, dialog); + super(eventService, notesService, dataService, optionsService, profileService, utilities, dialog); } override async addEmoji(e: { emoji: { native: any } }) { From 0183a9618dad643897e80bb203103e685021db0b Mon Sep 17 00:00:00 2001 From: d-krause Date: Thu, 6 Apr 2023 11:38:30 -0400 Subject: [PATCH 31/39] updating package.json to use npmjs version of animiq-nip76-tools --- package-lock.json | 42 +++++++++++++++++++++++++++++++++++++----- package.json | 2 +- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 765d52d6..c6fcc8b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "@twogate/ngx-photo-gallery": "^1.4.0", "@types/sharedworker": "^0.0.91", "angularx-qrcode": "^15.0.1", - "animiq-nip76-tools": "file:../../animiq-nip76-tools/dist", + "animiq-nip76-tools": "^1.0.5", "dexie": "^3.2.3", "html5-qrcode": "^2.3.7", "idb": "^7.1.1", @@ -4745,9 +4745,29 @@ } }, "node_modules/animiq-nip76-tools": { - "version": "1.0.0", - "resolved": "file:../../animiq-nip76-tools/dist", - "license": "MIT" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/animiq-nip76-tools/-/animiq-nip76-tools-1.0.5.tgz", + "integrity": "sha512-JumeH/1uTgTMEST5LOzeirprY7L0fQbFCien3UrhZN8Z31npSh8tK66gj+p9nG1+yCX5aYXN9LIxVadzndVDyA==", + "dependencies": { + "@noble/hashes": "^1.2.0" + }, + "peerDependencies": { + "@noble/secp256k1": "^1.7", + "@scure/base": "^1.1", + "nostr-tools": "^1.7.1", + "rxjs": "^7.0" + } + }, + "node_modules/animiq-nip76-tools/node_modules/@noble/hashes": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.0.tgz", + "integrity": "sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] }, "node_modules/ansi-colors": { "version": "4.1.3", @@ -16455,7 +16475,19 @@ } }, "animiq-nip76-tools": { - "version": "1.0.0" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/animiq-nip76-tools/-/animiq-nip76-tools-1.0.5.tgz", + "integrity": "sha512-JumeH/1uTgTMEST5LOzeirprY7L0fQbFCien3UrhZN8Z31npSh8tK66gj+p9nG1+yCX5aYXN9LIxVadzndVDyA==", + "requires": { + "@noble/hashes": "^1.2.0" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.0.tgz", + "integrity": "sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==" + } + } }, "ansi-colors": { "version": "4.1.3", diff --git a/package.json b/package.json index b343a046..d4f4595e 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@twogate/ngx-photo-gallery": "^1.4.0", "@types/sharedworker": "^0.0.91", "angularx-qrcode": "^15.0.1", - "animiq-nip76-tools": "file:../../animiq-nip76-tools/dist", + "animiq-nip76-tools": "^1.0.5", "dexie": "^3.2.3", "html5-qrcode": "^2.3.7", "idb": "^7.1.1", From a7bc30348461e569ab341e00cec5560a8687ad87 Mon Sep 17 00:00:00 2001 From: d-krause Date: Thu, 6 Apr 2023 12:10:08 -0400 Subject: [PATCH 32/39] cleanup demo and dev only changes --- src/app/app-routing.module.ts | 6 +----- src/app/app.module.ts | 2 +- src/app/connect/create/create.ts | 11 +++-------- src/app/connect/key/key.ts | 11 +++-------- src/app/types/storage.ts | 2 +- tsconfig.json | 10 +--------- 6 files changed, 10 insertions(+), 32 deletions(-) diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 2c132a68..127c5638 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -145,15 +145,11 @@ const routes: Routes = [ { path: 'private-channels', component: Nip76MainComponent, - canActivate: [Nip76DemoService], + canActivate: [AuthGuard], resolve: { data: LoadingResolverService, }, }, - { - path: 'private-channels/demo-setup', - component: Nip76DemoStarterComponent, - }, { path: 'private-channels/sent-rsvps', component: Nip76MainComponent, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index a4e766a4..6c99c32f 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -256,9 +256,9 @@ import { Nip76DemoStarterComponent, Nip76DemoCreateComponent, Nip76DemoKeyCompon BadgeCardComponent, TagsComponent, BadgeComponent, + ZappersListDialogComponent, Nip76MainComponent, AddChannelDialog, - ZappersListDialogComponent, Nip76EventButtonsComponent, Nip76EventThreadComponent, Nip76ContentComponent, diff --git a/src/app/connect/create/create.ts b/src/app/connect/create/create.ts index c219d337..aa92cedd 100644 --- a/src/app/connect/create/create.ts +++ b/src/app/connect/create/create.ts @@ -7,7 +7,6 @@ import { ThemeService } from '../../services/theme'; import { ProfileService } from '../../services/profile'; import { Utilities } from 'src/app/services/utilities'; import { DataService } from 'src/app/services/data'; -import { Nip76DemoService } from 'src/app/nip76/demo-only/nip76-demo.service'; @Component({ selector: 'app-create', @@ -32,8 +31,7 @@ export class CreateProfileComponent { private authService: AuthenticationService, public theme: ThemeService, private router: Router, - private security: SecurityService, - private nip76DemoService: Nip76DemoService + private security: SecurityService ) {} ngOnInit() { @@ -67,19 +65,16 @@ export class CreateProfileComponent { if (!this.privateKeyHex) { return; } - + if (!this.publicKeyHex) { return; } - + // First attempt to get public key from the private key to see if it's possible: const encrypted = await this.security.encryptData(this.privateKeyHex, this.password); const decrypted = await this.security.decryptData(encrypted, this.password); if (this.privateKeyHex == decrypted) { - - await this.nip76DemoService.createNip76Wallet(this.publicKeyHex, this.privateKeyHex); - localStorage.setItem('blockcore:notes:nostr:prvkey', encrypted); localStorage.setItem('blockcore:notes:nostr:pubkey', this.publicKeyHex); diff --git a/src/app/connect/key/key.ts b/src/app/connect/key/key.ts index 757d566f..cf528b38 100644 --- a/src/app/connect/key/key.ts +++ b/src/app/connect/key/key.ts @@ -3,7 +3,6 @@ import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { base64 } from '@scure/base'; import { relayInit, Relay, Event, utils, getPublicKey, nip19, nip06 } from 'nostr-tools'; -import { Nip76DemoService } from 'src/app/nip76/demo-only/nip76-demo.service'; import { SecurityService } from '../../services/security'; import { ThemeService } from '../../services/theme'; import { QrScanDialog } from './qr-scan-dialog/qr-scan'; @@ -23,8 +22,7 @@ export class ConnectKeyComponent { step = 1; mnemonic: string = ''; - constructor(public dialog: MatDialog, public theme: ThemeService, private router: Router, private security: SecurityService, - private nip76DemoService: Nip76DemoService) {} + constructor(public dialog: MatDialog, public theme: ThemeService, private router: Router, private security: SecurityService) { } setPrivateKey() { this.privateKeyHex = nip06.privateKeyFromSeedWords(this.mnemonic); @@ -45,7 +43,7 @@ export class ConnectKeyComponent { async persistKey() { // this.hidePrivateKey = true; - + setTimeout(async () => { if (!this.privateKeyHex) { return; @@ -58,11 +56,8 @@ export class ConnectKeyComponent { // First attempt to get public key from the private key to see if it's possible: const encrypted = await this.security.encryptData(this.privateKeyHex, this.password); const decrypted = await this.security.decryptData(encrypted, this.password); - - if (this.privateKeyHex == decrypted) { - - await this.nip76DemoService.createNip76Wallet(this.publicKeyHex, this.privateKeyHex); + if (this.privateKeyHex == decrypted) { localStorage.setItem('blockcore:notes:nostr:prvkey', encrypted); localStorage.setItem('blockcore:notes:nostr:pubkey', this.publicKeyHex); diff --git a/src/app/types/storage.ts b/src/app/types/storage.ts index b81a2080..3c000651 100644 --- a/src/app/types/storage.ts +++ b/src/app/types/storage.ts @@ -70,7 +70,7 @@ export class Storage { async open() { this.db = await openDB(this.name, 2, { upgrade(db, oldVersion, newVersion, transaction, event) { - // debugger; + debugger; switch (oldVersion) { case 0: diff --git a/tsconfig.json b/tsconfig.json index 15efd15f..892ba2fd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,15 +23,7 @@ "ES2022", "ESNext", "dom" - ], - "paths": { - "animiq-nip76-tools": [ - "../../animiq-nip76-tools/dist/" - ], - "@angular/*": [ - "./node_modules/@angular/*" - ] - }, + ] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, From e8898c4aeaa53762421e4fdea6d589882852af0b Mon Sep 17 00:00:00 2001 From: d-krause Date: Thu, 6 Apr 2023 12:11:27 -0400 Subject: [PATCH 33/39] cleanup demo and dev only changes --- .../nip76-demo-starter/nip76-demo-starter.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.ts b/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.ts index 9b79c8fe..b338c792 100644 --- a/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.ts +++ b/src/app/nip76/demo-only/nip76-demo-starter/nip76-demo-starter.component.ts @@ -60,7 +60,7 @@ export class Nip76DemoKeyComponent extends ConnectKeyComponent { nip76DemoService: Nip76DemoService ) { - super(dialog, theme, router1, security, nip76DemoService); + super(dialog, theme, router1, security); this.step = 3; } @@ -90,7 +90,7 @@ export class Nip76DemoCreateComponent extends CreateProfileComponent { security: SecurityService, nip76DemoService: Nip76DemoService ) { - super(utilities, dataService, profileService, authService, theme, router1, security, nip76DemoService) + super(utilities, dataService, profileService, authService, theme, router1, security) } override async persistKey() { From d9359007a62aa386966340111ee2fac369af9299 Mon Sep 17 00:00:00 2001 From: d-krause Date: Thu, 6 Apr 2023 12:21:53 -0400 Subject: [PATCH 34/39] cleanup demo and dev only changes --- src/app/connect/create/create.ts | 2 +- src/app/connect/key/key.ts | 2 +- .../nip76/nip76-event-buttons/nip76-event-buttons.component.ts | 2 +- .../nip76/nip76-event-thread/nip76-event-thread.component.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/connect/create/create.ts b/src/app/connect/create/create.ts index aa92cedd..f449e5ff 100644 --- a/src/app/connect/create/create.ts +++ b/src/app/connect/create/create.ts @@ -73,7 +73,7 @@ export class CreateProfileComponent { // First attempt to get public key from the private key to see if it's possible: const encrypted = await this.security.encryptData(this.privateKeyHex, this.password); const decrypted = await this.security.decryptData(encrypted, this.password); - + if (this.privateKeyHex == decrypted) { localStorage.setItem('blockcore:notes:nostr:prvkey', encrypted); localStorage.setItem('blockcore:notes:nostr:pubkey', this.publicKeyHex); diff --git a/src/app/connect/key/key.ts b/src/app/connect/key/key.ts index cf528b38..29aae4bd 100644 --- a/src/app/connect/key/key.ts +++ b/src/app/connect/key/key.ts @@ -22,7 +22,7 @@ export class ConnectKeyComponent { step = 1; mnemonic: string = ''; - constructor(public dialog: MatDialog, public theme: ThemeService, private router: Router, private security: SecurityService) { } + constructor(public dialog: MatDialog, public theme: ThemeService, private router: Router, private security: SecurityService) {} setPrivateKey() { this.privateKeyHex = nip06.privateKeyFromSeedWords(this.mnemonic); diff --git a/src/app/nip76/nip76-event-buttons/nip76-event-buttons.component.ts b/src/app/nip76/nip76-event-buttons/nip76-event-buttons.component.ts index f128d0cc..da3a271c 100644 --- a/src/app/nip76/nip76-event-buttons/nip76-event-buttons.component.ts +++ b/src/app/nip76/nip76-event-buttons/nip76-event-buttons.component.ts @@ -8,7 +8,7 @@ import { NostrEventDocument } from 'src/app/services/interfaces'; import { OptionsService } from 'src/app/services/options'; import { ProfileService } from 'src/app/services/profile'; import { Utilities } from 'src/app/services/utilities'; -import { PostDocument } from '../../../../../../animiq-nip76-tools/dist/src'; +import { PostDocument } from 'animiq-nip76-tools'; import { EventButtonsComponent } from '../../shared/event-buttons/event-buttons'; import { Nip76Service } from '../nip76.service'; import { NotesService } from 'src/app/services/notes'; diff --git a/src/app/nip76/nip76-event-thread/nip76-event-thread.component.ts b/src/app/nip76/nip76-event-thread/nip76-event-thread.component.ts index d7d69e71..1e460e75 100644 --- a/src/app/nip76/nip76-event-thread/nip76-event-thread.component.ts +++ b/src/app/nip76/nip76-event-thread/nip76-event-thread.component.ts @@ -1,5 +1,5 @@ import { Component, Input } from '@angular/core'; -import { PostDocument } from '../../../../../../animiq-nip76-tools/dist/src'; +import { PostDocument } from 'animiq-nip76-tools'; @Component({ selector: 'app-nip76-event-thread', From b142b676ed7568df4e1e68db0cfa1e5c8b327fad Mon Sep 17 00:00:00 2001 From: d-krause Date: Thu, 6 Apr 2023 12:30:44 -0400 Subject: [PATCH 35/39] cleanup demo and dev only changes removed --- src/app/profile/profile.html | 3 +-- src/app/profile/profile.ts | 8 +++----- src/app/relays/relays.html | 6 ++---- src/app/relays/relays.ts | 7 ------- 4 files changed, 6 insertions(+), 18 deletions(-) diff --git a/src/app/profile/profile.html b/src/app/profile/profile.html index 14e8feb0..03d908b0 100644 --- a/src/app/profile/profile.html +++ b/src/app/profile/profile.html @@ -2,9 +2,8 @@
    - Manage Private Threads - +

    diff --git a/src/app/profile/profile.ts b/src/app/profile/profile.ts index 1c0ee47e..32832af0 100644 --- a/src/app/profile/profile.ts +++ b/src/app/profile/profile.ts @@ -49,12 +49,10 @@ export class ProfileComponent { private profileService: ProfileService, private dataService: DataService, private activatedRoute: ActivatedRoute - ) { - - this.appState.updateTitle('Edit Profile'); - } - + ) {} + async ngOnInit() { + this.appState.updateTitle('Edit Profile'); this.originalProfile = { name: '', diff --git a/src/app/relays/relays.html b/src/app/relays/relays.html index fb4d9945..c9b70645 100644 --- a/src/app/relays/relays.html +++ b/src/app/relays/relays.html @@ -9,10 +9,8 @@

    - - - +
    --> +


    diff --git a/src/app/relays/relays.ts b/src/app/relays/relays.ts index d0c90d89..64bd9065 100644 --- a/src/app/relays/relays.ts +++ b/src/app/relays/relays.ts @@ -92,13 +92,6 @@ export class RelaysManagementComponent { this.wipedNotes = true; } - async getLocalDevRelays() { - const relays = { - 'wss://nostr-dev.animiq.com' : { read: true, write: true } - } - await this.relayService.appendRelays(relays); - } - async getDefaultRelays() { // Append the default relays. await this.relayService.appendRelays(this.nostr.defaultRelays); From f07b0f279d80b25f3fb384e82ce3ec2d5f421844 Mon Sep 17 00:00:00 2001 From: d-krause Date: Thu, 6 Apr 2023 12:32:42 -0400 Subject: [PATCH 36/39] cleanup demo and dev only changes removed --- src/app/profile/profile.ts | 2 +- src/app/relays/relays.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/profile/profile.ts b/src/app/profile/profile.ts index 32832af0..2945ba3a 100644 --- a/src/app/profile/profile.ts +++ b/src/app/profile/profile.ts @@ -50,7 +50,7 @@ export class ProfileComponent { private dataService: DataService, private activatedRoute: ActivatedRoute ) {} - + async ngOnInit() { this.appState.updateTitle('Edit Profile'); diff --git a/src/app/relays/relays.html b/src/app/relays/relays.html index c9b70645..e30c28e2 100644 --- a/src/app/relays/relays.html +++ b/src/app/relays/relays.html @@ -9,7 +9,7 @@
    +
    -->
    Date: Sun, 23 Apr 2023 11:56:59 -0400 Subject: [PATCH 37/39] WORK IN PROGRESS: refactors for supporting blockcore-wallet extensions --- package.json | 2 +- .../nip76-add-invitation.component.ts | 14 +- .../nip76-channel.component.html | 8 +- .../nip76-channel/nip76-channel.component.ts | 12 +- .../nip76-diagnostics.component.ts | 2 +- .../nip76/nip76-main/nip76-main.component.ts | 6 +- .../nip76-rsvps-sent.component.ts | 3 +- src/app/nip76/nip76.service.ts | 150 ++++++++++++++---- 8 files changed, 139 insertions(+), 58 deletions(-) diff --git a/package.json b/package.json index 8a529472..d8ab08dc 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@twogate/ngx-photo-gallery": "^1.4.0", "@types/sharedworker": "^0.0.91", "angularx-qrcode": "^15.0.1", - "animiq-nip76-tools": "^1.0.5", + "animiq-nip76-tools": "file:../../animiq-nip76-tools/dist", "dexie": "^3.2.3", "html5-qrcode": "^2.3.7", "idb": "^7.1.1", diff --git a/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.ts b/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.ts index d8a246d6..186065ad 100644 --- a/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.ts +++ b/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.ts @@ -72,19 +72,23 @@ export class Nip76AddInvitationComponent { async copyInviteWithoutSave() { if (this.valid) { let pointer: string; - const threadPointer = { + const threadPointer: nip19Extension.PrivateChannelPointer = { type: 0, docIndex: -1, signingKey: this.data.channel.dkxPost.signingParent!.publicKey, signingChain: this.data.channel.dkxPost.signingParent!.chainCode, - cryptoKey: this.data.channel.dkxPost.cryptoParent.publicKey, - cryptoChain: this.data.channel.dkxPost.cryptoParent.chainCode, + cryptoKey: this.data.channel.dkxPost.encryptParent.publicKey, + cryptoChain: this.data.channel.dkxPost.encryptParent.chainCode, }; if (this.data.invitationType === 'password') { pointer = await nip19Extension.nprivateChannelEncode(threadPointer, this.data.password!); } else { - const privateKey = await this.nip76Service.passwordDialog('Save RSVP'); - pointer = await nip19Extension.nprivateChannelEncode(threadPointer, privateKey, this.data.validPubkey); + if (this.nip76Service.extensionProvider) { + pointer = await (globalThis as any).nostr.nip76.createInvitation(threadPointer, this.data.validPubkey); + } else { + const privateKey = await this.nip76Service.passwordDialog('Save RSVP'); + pointer = await nip19Extension.nprivateChannelEncode(threadPointer, privateKey, this.data.validPubkey); + } } navigator.clipboard.writeText(pointer); this.snackBar.open(`The invitation is now in your clipboard.`, 'Hide', defaultSnackBarOpts); diff --git a/src/app/nip76/nip76-channel/nip76-channel.component.html b/src/app/nip76/nip76-channel/nip76-channel.component.html index a100a818..8f317af9 100644 --- a/src/app/nip76/nip76-channel/nip76-channel.component.html +++ b/src/app/nip76/nip76-channel/nip76-channel.component.html @@ -81,10 +81,10 @@ - Password Protected {{ !invite.content.cryptoParent ? " (Suspended)" : ""}} + Password Protected {{ !invite.content.encryptParent ? " (Suspended)" : ""}} - (Suspended) @@ -103,12 +103,12 @@ Copy Suspend Reinstate diff --git a/src/app/nip76/nip76-channel/nip76-channel.component.ts b/src/app/nip76/nip76-channel/nip76-channel.component.ts index c1575d43..acbabdf5 100644 --- a/src/app/nip76/nip76-channel/nip76-channel.component.ts +++ b/src/app/nip76/nip76-channel/nip76-channel.component.ts @@ -106,8 +106,9 @@ export class Nip76ChannelHeaderComponent { } async deleteRSVP(rsvp: Rsvp) { - const privateKey = await this.nip76Service.passwordDialog('Delete RSVP'); - if (privateKey) { + const privateKeyRequired = !this.nip76Service.extensionProvider; + const privateKey = privateKeyRequired ? await this.nip76Service.passwordDialog('Delete RSVP') : undefined; + if (privateKey || !privateKeyRequired) { const walletRsvp = this.wallet.rsvps.find(x => x.content.pubkey === this.wallet.ownerPubKey && x.content.pointerDocIndex === rsvp.content.pointerDocIndex); if (await this.nip76Service.deleteDocument(rsvp, privateKey)) { rsvp.dkxParent.documents.splice(rsvp.dkxParent.documents.indexOf(rsvp), 1); @@ -121,11 +122,8 @@ export class Nip76ChannelHeaderComponent { } async deleteInvitation(invite: Invitation) { - const privateKey = await this.nip76Service.passwordDialog('Delete Invitation'); - if (privateKey) { - if (await this.nip76Service.deleteDocument(invite, privateKey)) { - invite.dkxParent.documents.splice(invite.dkxParent.documents.indexOf(invite), 1); - } + if (await this.nip76Service.deleteDocument(invite)) { + invite.dkxParent.documents.splice(invite.dkxParent.documents.indexOf(invite), 1); } } diff --git a/src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.ts b/src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.ts index 362c95a7..ca29bf6c 100644 --- a/src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.ts +++ b/src/app/nip76/nip76-diagnostics/nip76-diagnostics.component.ts @@ -87,7 +87,7 @@ export class Nip76DiagnosticsComponent { prettierJson(forCopy = false): SafeHtml | string { const keys: string[] = []; const ignoreKeys = ['channelSubscription', 'documents']; - const privateDataKeys = ['xpriv', "xpub", "wordset", "password", "signingKey", "cryptoKey"]; + const privateDataKeys = ['xpriv', "xpub", "wordset", "password", "signingKey", "encryptKey", "signingParent", "encryptParent"]; const replacer = (k: string, v: any) => { if (this.diagType === DiagType.Document && ignoreKeys.includes(k)) { return undefined; diff --git a/src/app/nip76/nip76-main/nip76-main.component.ts b/src/app/nip76/nip76-main/nip76-main.component.ts index b3926388..e505f693 100644 --- a/src/app/nip76/nip76-main/nip76-main.component.ts +++ b/src/app/nip76/nip76-main/nip76-main.component.ts @@ -77,11 +77,7 @@ export class Nip76MainComponent { } async initPrivateChannels() { - const publicKey = this.nip76Service.profile.pubkey; - const privateKey = await this.nip76Service.passwordDialog('Create an HD Wallet'); - this.nip76Service.wallet = await Nip76WebWalletStorage.fromStorage({ publicKey, privateKey }); - this.nip76Service.wallet.saveWallet(privateKey); - location.reload(); + await this.nip76Service.loadWallet(); } copyDemoInvitation(name: 'Alice') { diff --git a/src/app/nip76/nip76-rsvps-sent/nip76-rsvps-sent.component.ts b/src/app/nip76/nip76-rsvps-sent/nip76-rsvps-sent.component.ts index 7a7f0e72..be5eb561 100644 --- a/src/app/nip76/nip76-rsvps-sent/nip76-rsvps-sent.component.ts +++ b/src/app/nip76/nip76-rsvps-sent/nip76-rsvps-sent.component.ts @@ -27,7 +27,8 @@ export class Nip76RsvpsSentComponent { } async deleteRSVP(rsvp: Rsvp) { - const privateKey = await this.nip76Service.passwordDialog('Delete RSVP'); + const privateKeyRequired = !this.nip76Service.extensionProvider; + const privateKey = privateKeyRequired ? await this.nip76Service.passwordDialog('Delete RSVP') : undefined; if (privateKey) { const channelRsvp = rsvp.channel?.rsvps.find(x => x.content.pubkey === this.wallet.ownerPubKey && x.content.pointerDocIndex === rsvp.content.pointerDocIndex); if (await this.nip76Service.deleteDocument(rsvp, privateKey)) { diff --git a/src/app/nip76/nip76.service.ts b/src/app/nip76/nip76.service.ts index 4aadafc9..e39b213d 100644 --- a/src/app/nip76/nip76.service.ts +++ b/src/app/nip76/nip76.service.ts @@ -3,9 +3,9 @@ import { MatDialog } from '@angular/material/dialog'; import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar'; import { bech32 } from '@scure/base'; import { - ContentDocument, HDKey, getNowSeconds, + ContentDocument, HDKey, getNowSeconds, Nip76WalletConstructorArgs, HDKIndex, HDKIndexType, Invitation, nip19Extension, Nip76Wallet, - Nip76WebWalletStorage, NostrKinds, PostDocument, PrivateChannel, Rsvp, Versions, walletRsvpDocumentsOffset + Nip76WebWalletStorage, NostrKinds, PostDocument, PrivateChannel, Rsvp, Versions, walletRsvpDocumentsOffset, NostrEventDocument } from 'animiq-nip76-tools'; import * as nostrTools from 'nostr-tools'; import { filter, firstValueFrom, Subject, take } from 'rxjs'; @@ -51,17 +51,48 @@ export class Nip76Service { ) { this.profileService.profile$.pipe(filter(x => !!x), take(1)).subscribe(profile => { this.profile = profile!; - Nip76WebWalletStorage.fromStorage({ publicKey: profile!.pubkey }).then(wallet => { - this.wallet = wallet; - if (this.wallet.isInSession) { - setTimeout(() => { this.loadDocuments(); }, 500); - } else if (!this.wallet.isGuest) { - this.login(); - } - }); + this.loadWallet(); }); } + get extensionProvider(): any { + return (globalThis as any).nostr?.nip76; + } + + async loadWallet() { + const publicKey = this.profile.pubkey; + if (this.extensionProvider) { + const args = await this.extensionProvider.getWalletArgs(); + const rootKey = HDKey.parseExtendedKey(args.rootKey.xpriv); + const walletArgs: Nip76WalletConstructorArgs = { + publicKey: args.publicKey, + wordset: Uint32Array.from(Object.values(args.wordset)), + rootKey, + store: {} as Nip76WebWalletStorage, + isGuest: false, + isInSession: true, + }; + this.wallet = new Nip76Wallet(walletArgs); + setTimeout(() => { this.loadDocuments(); }, 500); + } else if (localStorage.getItem(nostrPrivKeyAddress)) { + if (localStorage.getItem(Nip76WebWalletStorage.backupKey)) { + Nip76WebWalletStorage.fromStorage({ publicKey }).then(wallet => { + this.wallet = wallet; + if (this.wallet.isInSession) { + setTimeout(() => { this.loadDocuments(); }, 500); + } else if (!this.wallet.isGuest) { + this.login(); + } + }); + } else { + const privateKey = await this.passwordDialog('Create an HD Wallet'); + this.wallet = await Nip76WebWalletStorage.fromStorage({ publicKey, privateKey }); + this.wallet.saveWallet(privateKey); + location.reload(); + } + } + } + async passwordDialog(actionPrompt: string): Promise { return new Promise((resolve, reject) => { const dialogRef = this.dialog.open(PasswordDialog, { @@ -229,11 +260,11 @@ export class Nip76Service { } async readInvitation(invite: Invitation): Promise { - if (!invite.content.signingParent && !invite.content.cryptoParent) { + if (!invite.content.signingParent && !invite.content.encryptParent) { this.snackBar.open(`Encountered a suspended invitation from ${invite.ownerPubKey}`, 'Hide', defaultSnackBarOpts); return undefined; } - const channelIndex = new HDKIndex(HDKIndexType.Singleton, invite.content.signingParent!, invite.content.cryptoParent!); + const channelIndex = new HDKIndex(HDKIndexType.Singleton, invite.content.signingParent!, invite.content.encryptParent!); const channelIndex$ = new Subject(); const channelIndexSub = this.relayService.subscribe( [{ authors: [channelIndex.signingParent.nostrPubKey], kinds: [17761], limit: 1 }], @@ -270,11 +301,21 @@ export class Nip76Service { try { const words = bech32.decode(channelPointer, 5000).words; const pointerType = Uint8Array.from(bech32.fromWords(words))[0] as nip19Extension.PointerType; + let pointer: nip19Extension.PrivateChannelPointer; + if ((pointerType & nip19Extension.PointerType.SharedSecret) == nip19Extension.PointerType.SharedSecret) { - secret = await this.passwordDialog('Preview Private Invitation'); + if (this.extensionProvider) { + const pointerDTO = await this.extensionProvider.readInvitation(channelPointer); + pointer = nip19Extension.pointerFromDTO(pointerDTO); + } else { + secret = await this.passwordDialog('Preview Private Invitation'); + const p = await nip19Extension.decode(channelPointer, secret!); + pointer = p.data as nip19Extension.PrivateChannelPointer; + } + } else { + const p = await nip19Extension.decode(channelPointer, secret!); + pointer = p.data as nip19Extension.PrivateChannelPointer; } - const p = await nip19Extension.decode(channelPointer, secret!); - const pointer = p.data as nip19Extension.PrivateChannelPointer; if (pointer) { if ((pointer.type & nip19Extension.PointerType.FullKeySet) === nip19Extension.PointerType.FullKeySet) { @@ -288,7 +329,7 @@ export class Nip76Service { pubkey: signingParent.nostrPubKey, docIndex: pointer.docIndex, signingParent, - cryptoParent + encryptParent: cryptoParent }; invite.ready = true; return this.readInvitation(invite); @@ -328,28 +369,37 @@ export class Nip76Service { } async saveChannel(channel: PrivateChannel, privateKey?: string) { - privateKey = privateKey || await this.passwordDialog('Save Channel Details'); channel.content.created_at = channel.content.created_at || channel?.nostrEvent.created_at || getNowSeconds(); - const ev = await this.wallet.documentsIndex.createEvent(channel, privateKey); - await this.dataService.publishEvent(ev); + let event: NostrEventDocument; + if (this.extensionProvider) { + event = await this.extensionProvider.createEvent(this.wallet.documentsIndex, channel); + } else { + privateKey = privateKey || await this.passwordDialog('Save Channel Details'); + event = await this.wallet.documentsIndex.createEvent(channel, privateKey); + } + await this.dataService.publishEvent(event); return true; } async saveNote(channel: PrivateChannel, text: string) { - const privateKey = await this.passwordDialog('Save Note'); const postDocument = new PostDocument(); postDocument.content = { text, pubkey: this.wallet.ownerPubKey, kind: nostrTools.Kind.Text } - const event = await channel.dkxPost.createEvent(postDocument, privateKey); + let event: NostrEventDocument; + if (this.extensionProvider) { + event = await this.extensionProvider.createEvent(channel.dkxPost, postDocument); + } else { + const privateKey = await this.passwordDialog('Save Note'); + event = await channel.dkxPost.createEvent(postDocument, privateKey); + } await this.dataService.publishEvent(event); return true; } async saveReaction(post: PostDocument, text: string, kind: nostrTools.Kind): Promise { - const privateKey = await this.passwordDialog('Save ' + (kind === nostrTools.Kind.Reaction ? 'Reaction' : 'Reply')); const postDocument = new PostDocument(); postDocument.content = { kind, @@ -357,13 +407,18 @@ export class Nip76Service { text, tags: [['e', post.nostrEvent.id]] }; - const event = await post.dkxParent.createEvent(postDocument, privateKey); + let event: NostrEventDocument; + if (this.extensionProvider) { + event = await this.extensionProvider.createEvent(post.dkxParent, postDocument); + } else { + const privateKey = await this.passwordDialog('Save ' + (kind === nostrTools.Kind.Reaction ? 'Reaction' : 'Reply')); + event = await post.dkxParent.createEvent(postDocument, privateKey); + } await this.dataService.publishEvent(event); return postDocument; } async saveInvitation(channel: PrivateChannel, invitation: AddInvitationDialogData): Promise { - const privateKey = await this.passwordDialog('Save Invitation'); const invite = new Invitation(); invite.docIndex = channel.dkxInvite.documents.length + 1; invite.content = { @@ -373,46 +428,73 @@ export class Nip76Service { password: invitation.invitationType === 'password' ? invitation.password : undefined, pubkey: channel.dkxPost.signingParent.nostrPubKey, signingParent: channel.dkxPost.signingParent, - cryptoParent: channel.dkxPost.cryptoParent, + encryptParent: channel.dkxPost.encryptParent, + }; + let event: NostrEventDocument; + if (this.extensionProvider) { + event = await this.extensionProvider.createEvent(channel.dkxInvite, invite); + } else { + const privateKey = await this.passwordDialog('Save Invitation'); + event = await channel.dkxInvite.createEvent(invite, privateKey); } - const event = await channel.dkxInvite.createEvent(invite, privateKey); await this.dataService.publishEvent(event); return invite; } async resaveInvitation(channel: PrivateChannel, invite: Invitation, withKeys: boolean): Promise { - const privateKey = await this.passwordDialog('Revoke Invitation'); invite.content.signingParent = withKeys ? channel.dkxPost.signingParent : undefined; - invite.content.cryptoParent = withKeys ? channel.dkxPost.cryptoParent : undefined; - const event = await channel.dkxInvite.createEvent(invite, privateKey); + invite.content.encryptParent = withKeys ? channel.dkxPost.encryptParent : undefined; + let event: NostrEventDocument; + if (this.extensionProvider) { + event = await this.extensionProvider.createEvent(channel.dkxInvite, invite); + } else { + const privateKey = await this.passwordDialog((withKeys ? 'Reinstate' : 'Suspend') + ' Invitation'); + event = await channel.dkxInvite.createEvent(invite, privateKey); + } await this.dataService.publishEvent(event); return invite; } async saveRSVP(channel: PrivateChannel) { - const privateKey = await this.passwordDialog('Save RSVP'); const rsvp = new Rsvp(); rsvp.content = { kind: NostrKinds.PrivateChannelRSVP, pubkey: this.wallet.ownerPubKey, pointerDocIndex: channel.invitation.pointer.docIndex, type: channel.invitation.pointer.type, + }; + let event1: NostrEventDocument; + let privateKey: string; + if (this.extensionProvider) { + event1 = await this.extensionProvider.createEvent(channel.dkxRsvp, rsvp); + } else { + privateKey = await this.passwordDialog('Save RSVP'); + event1 = await channel.dkxRsvp.createEvent(rsvp, privateKey); } - const event1 = await channel.dkxRsvp.createEvent(rsvp, privateKey); await this.dataService.publishEvent(event1); rsvp.docIndex = channel.invitation.docIndex || (this.wallet.rsvps.length + 1 + walletRsvpDocumentsOffset); rsvp.content.signingKey = channel.invitation.pointer.signingKey; rsvp.content.cryptoKey = channel.invitation.pointer.cryptoKey; - const event2 = await this.wallet.documentsIndex.createEvent(rsvp, privateKey); + let event2: NostrEventDocument; + if (this.extensionProvider) { + event2 = await this.extensionProvider.createEvent(this.wallet.documentsIndex, rsvp); + } else { + event2 = await this.wallet.documentsIndex.createEvent(rsvp, privateKey!); + } await this.dataService.publishEvent(event2); return true; } async deleteDocument(doc: ContentDocument, privateKey?: string) { - privateKey = privateKey || await this.passwordDialog('Delete Document'); - const event = await doc.dkxParent.createDeleteEvent(doc, privateKey); + let event: NostrEventDocument; + if (this.extensionProvider) { + event = await this.extensionProvider.createDeleteEvent(doc); + } else { + privateKey = privateKey || await this.passwordDialog('Delete Document'); + event = await doc.dkxParent.createDeleteEvent(doc, privateKey); + } if (doc.nostrEvent.pubkey !== event.pubkey) { this.snackBar.open(`Cannot delete another user's document.`, 'Hide', defaultSnackBarOpts); return false; From db973ae2b43dc4de9b9334d4688b265e70a86798 Mon Sep 17 00:00:00 2001 From: d-krause Date: Tue, 25 Apr 2023 10:28:00 -0400 Subject: [PATCH 38/39] blockcore-wallet no longer exposes any private channel private keys to caller --- .../nip76-channel/nip76-channel.component.ts | 2 +- src/app/nip76/nip76.service.ts | 50 +++++++++++-------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/app/nip76/nip76-channel/nip76-channel.component.ts b/src/app/nip76/nip76-channel/nip76-channel.component.ts index acbabdf5..2bdf9f7f 100644 --- a/src/app/nip76/nip76-channel/nip76-channel.component.ts +++ b/src/app/nip76/nip76-channel/nip76-channel.component.ts @@ -76,7 +76,7 @@ export class Nip76ChannelHeaderComponent { } get isOwner(): boolean { - return !!this.channel.dkxPost.signingParent.privateKey; + return !!this.channel.dkxInvite; } async updateProfileDetails() { diff --git a/src/app/nip76/nip76.service.ts b/src/app/nip76/nip76.service.ts index e39b213d..8bab8585 100644 --- a/src/app/nip76/nip76.service.ts +++ b/src/app/nip76/nip76.service.ts @@ -4,8 +4,9 @@ import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar'; import { bech32 } from '@scure/base'; import { ContentDocument, HDKey, getNowSeconds, Nip76WalletConstructorArgs, - HDKIndex, HDKIndexType, Invitation, nip19Extension, Nip76Wallet, - Nip76WebWalletStorage, NostrKinds, PostDocument, PrivateChannel, Rsvp, Versions, walletRsvpDocumentsOffset, NostrEventDocument + HDKIndex, HDKIndexDTO, HDKIndexType, Invitation, nip19Extension, Nip76Wallet, + Nip76WebWalletStorage, NostrKinds, PostDocument, PrivateChannel, Rsvp, Versions, walletRsvpDocumentsOffset, NostrEventDocument, + SequentialKeysetDTO } from 'animiq-nip76-tools'; import * as nostrTools from 'nostr-tools'; import { filter, firstValueFrom, Subject, take } from 'rxjs'; @@ -63,11 +64,14 @@ export class Nip76Service { const publicKey = this.profile.pubkey; if (this.extensionProvider) { const args = await this.extensionProvider.getWalletArgs(); - const rootKey = HDKey.parseExtendedKey(args.rootKey.xpriv); + const rootKey = HDKey.parseExtendedKey(args.rootKey); + const wordset = Uint32Array.from(args.wordset); + const documentsIndex: HDKIndex = HDKIndex.fromJSON(args.documentsIndex); const walletArgs: Nip76WalletConstructorArgs = { publicKey: args.publicKey, - wordset: Uint32Array.from(Object.values(args.wordset)), + wordset, rootKey, + documentsIndex, store: {} as Nip76WebWalletStorage, isGuest: false, isInSession: true, @@ -142,22 +146,26 @@ export class Nip76Service { this.wallet = await Nip76WebWalletStorage.fromStorage({ publicKey: this.wallet.ownerPubKey }); } - loadDocuments(start = 1, length = 20) { - const channelPubkeys = this.wallet.documentsIndex.getDocKeys(start, length); - const invitePubkeys = this.wallet.documentsIndex.getDocKeys(start + walletRsvpDocumentsOffset, length); + loadDocuments(start = 0) { + const channelPubkeys = this.wallet.documentsIndex.getSequentialKeyset(0, 0); + const invitePubkeys = this.wallet.documentsIndex.getSequentialKeyset(walletRsvpDocumentsOffset, 0); if (this.documentsSubscription) { this.relayService.unsubscribe(this.documentsSubscription.id); } - const privateNotes$ = new Subject(); - privateNotes$.subscribe(async nostrEvent => { - let docIndex = channelPubkeys.findIndex(x => x === nostrEvent.pubkey) + start; - if (docIndex) { + const privateDoc$ = new Subject(); + privateDoc$.subscribe(async nostrEvent => { + let docIndex = channelPubkeys.keys.findIndex(x => x.signingKey?.nostrPubKey === nostrEvent.pubkey) + start; + if (docIndex > -1) { const doc = await this.wallet.documentsIndex.readEvent(nostrEvent, docIndex) as PrivateChannel; + if(this.extensionProvider) { + const dkxInviteDTO = await this.extensionProvider.getInvitationIndex(docIndex); + doc.dkxInvite = HDKIndex.fromJSON(dkxInviteDTO); + } if (doc) { this.loadChannel(doc); } } else { - docIndex = invitePubkeys.findIndex(x => x === nostrEvent.pubkey) + start + walletRsvpDocumentsOffset; + docIndex = invitePubkeys.keys.findIndex(x => x.signingKey?.nostrPubKey === nostrEvent.pubkey) + start + walletRsvpDocumentsOffset; const doc = await this.wallet.documentsIndex.readEvent(nostrEvent, docIndex); if (doc && doc instanceof Rsvp) { const pointer: nip19Extension.PrivateChannelPointer = { @@ -173,15 +181,15 @@ export class Nip76Service { } }); const filters = [ - { authors: channelPubkeys, kinds: [17761], limit: length }, - { authors: invitePubkeys, kinds: [17761], limit: length } + { authors: channelPubkeys.keys.map(x => x.signingKey?.nostrPubKey!), kinds: [17761], limit: channelPubkeys.keys.length }, + { authors: invitePubkeys.keys.map(x => x.signingKey?.nostrPubKey!), kinds: [17761], limit: invitePubkeys.keys.length } ]; this.documentsSubscription = this.relayService.subscribe(filters, - `nip76Service.loadDocuments.${start}-${length}`, 'Replaceable', privateNotes$); + `nip76Service.loadDocuments.${start}-${length}`, 'Replaceable', privateDoc$); } - loadChannel(channel: PrivateChannelWithRelaySub, start = 1, length = 20) { - const invitePubs = channel.dkxInvite?.getDocKeys(start, length); + loadChannel(channel: PrivateChannelWithRelaySub, start = 0) { + const invitePubs = channel.dkxInvite?.getSequentialKeyset(0, 0); if (channel.channelSubscription) { this.relayService.unsubscribe(channel.channelSubscription.id); } @@ -192,16 +200,16 @@ export class Nip76Service { } else if (channel.dkxRsvp.eventTag === nostrEvent.tags[0][1]) { const rsvp = await channel.dkxRsvp.readEvent(nostrEvent); } else if (invitePubs) { - const docIndex = invitePubs.findIndex(x => x === nostrEvent.pubkey) + start; + const docIndex = invitePubs.keys.findIndex(x => x.signingKey?.nostrPubKey === nostrEvent.pubkey) + start; const invite = await channel.dkxInvite.readEvent(nostrEvent, docIndex); } }); const filters: nostrTools.Filter[] = [ - { '#e': [channel.dkxPost.eventTag], kinds: [17761], limit: length }, - { '#e': [channel.dkxRsvp.eventTag], kinds: [17761], limit: length }, + { '#e': [channel.dkxPost.eventTag], kinds: [17761], limit: 100 }, + { '#e': [channel.dkxRsvp.eventTag], kinds: [17761], limit: 100 }, ]; if (invitePubs) { - filters.push({ authors: invitePubs, kinds: [17761], limit: 100 }) + filters.push({ authors: invitePubs.keys.map(x => x.signingKey?.nostrPubKey!), kinds: [17761], limit: invitePubs.keys.length }) } channel.channelSubscription = this.relayService.subscribe( filters, From 88e8ab5563a1da7012bdf2126e5a57526bccb7a9 Mon Sep 17 00:00:00 2001 From: d-krause Date: Wed, 26 Apr 2023 09:44:35 -0400 Subject: [PATCH 39/39] refactor moving key management from wallet to web storage. streamlining extension provider methods and adding interfaces. --- .../nip76-add-invitation.component.ts | 2 +- .../nip76-channel/nip76-channel.component.ts | 18 +++- .../nip76-main/nip76-main.component.html | 14 ++-- .../nip76/nip76-main/nip76-main.component.ts | 4 +- src/app/nip76/nip76.service.ts | 82 +++++++++---------- 5 files changed, 62 insertions(+), 58 deletions(-) diff --git a/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.ts b/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.ts index 186065ad..1d7e1c15 100644 --- a/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.ts +++ b/src/app/nip76/nip76-add-invitation/nip76-add-invitation.component.ts @@ -84,7 +84,7 @@ export class Nip76AddInvitationComponent { pointer = await nip19Extension.nprivateChannelEncode(threadPointer, this.data.password!); } else { if (this.nip76Service.extensionProvider) { - pointer = await (globalThis as any).nostr.nip76.createInvitation(threadPointer, this.data.validPubkey); + pointer = await this.nip76Service.extensionProvider.createInvitation(threadPointer, this.data.validPubkey!); } else { const privateKey = await this.nip76Service.passwordDialog('Save RSVP'); pointer = await nip19Extension.nprivateChannelEncode(threadPointer, privateKey, this.data.validPubkey); diff --git a/src/app/nip76/nip76-channel/nip76-channel.component.ts b/src/app/nip76/nip76-channel/nip76-channel.component.ts index 2bdf9f7f..53f134bc 100644 --- a/src/app/nip76/nip76-channel/nip76-channel.component.ts +++ b/src/app/nip76/nip76-channel/nip76-channel.component.ts @@ -96,12 +96,24 @@ export class Nip76ChannelHeaderComponent { } cancelAdd() { - const index = this.wallet.documentsIndex.documents.findIndex(x => this.channel); - this.wallet.documentsIndex.documents.splice(index, 1); + const index = this.wallet.documentsIndex!.documents.findIndex(x => this.channel); + this.wallet.documentsIndex!.documents.splice(index, 1); } async copyKeys(invite: Invitation) { - navigator.clipboard.writeText(await invite.getPointer()); + let invitation: string; + if (this.nip76Service.extensionProvider && invite.content.for) { + const keyset = invite.dkxParent.getDocumentKeyset(invite.docIndex); + invitation = await this.nip76Service.extensionProvider.createInvitation({ + type: 0, + docIndex: invite.docIndex, + signingKey: keyset.signingKey!.publicKey, + cryptoKey: keyset.encryptKey!.publicKey, + }, invite.content.for); + } else { + invitation = await invite.getPointer(); + } + navigator.clipboard.writeText(invitation); this.snackBar.open(`The invitation is now in your clipboard.`, 'Hide', defaultSnackBarOpts); } diff --git a/src/app/nip76/nip76-main/nip76-main.component.html b/src/app/nip76/nip76-main/nip76-main.component.html index 811144fb..9b5b2eb4 100644 --- a/src/app/nip76/nip76-main/nip76-main.component.html +++ b/src/app/nip76/nip76-main/nip76-main.component.html @@ -1,28 +1,28 @@
    - - - -
    - - @@ -38,7 +38,7 @@

    NIP 76 Private Channels are not initialized for this profile yet.

    -
    +
    Thanks for coming to checkout the Nip76 Private Channels Demo.

    diff --git a/src/app/nip76/nip76-main/nip76-main.component.ts b/src/app/nip76/nip76-main/nip76-main.component.ts index e505f693..a2747a8a 100644 --- a/src/app/nip76/nip76-main/nip76-main.component.ts +++ b/src/app/nip76/nip76-main/nip76-main.component.ts @@ -38,7 +38,7 @@ export class Nip76MainComponent { this.activeChannelId = params.get('channelPubKey'); }); setTimeout(() => { - this.showHelp = this.wallet?.isInSession && this.wallet?.channels?.length === 0; + this.showHelp = this.wallet?.isReady && this.wallet?.channels?.length === 0; }, 3000); } @@ -65,7 +65,7 @@ export class Nip76MainComponent { get activeChannel(): PrivateChannel | undefined { if (this.activeChannelId) { - if (!this.wallet.isGuest && this.wallet.isInSession) { + if (!this.wallet.isGuest && this.wallet.isReady) { const channel = this.nip76Service.findChannel(this.activeChannelId); if (channel) { return channel; diff --git a/src/app/nip76/nip76.service.ts b/src/app/nip76/nip76.service.ts index 8bab8585..4aaa883f 100644 --- a/src/app/nip76/nip76.service.ts +++ b/src/app/nip76/nip76.service.ts @@ -3,10 +3,8 @@ import { MatDialog } from '@angular/material/dialog'; import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar'; import { bech32 } from '@scure/base'; import { - ContentDocument, HDKey, getNowSeconds, Nip76WalletConstructorArgs, - HDKIndex, HDKIndexDTO, HDKIndexType, Invitation, nip19Extension, Nip76Wallet, - Nip76WebWalletStorage, NostrKinds, PostDocument, PrivateChannel, Rsvp, Versions, walletRsvpDocumentsOffset, NostrEventDocument, - SequentialKeysetDTO + ContentDocument, getNowSeconds, HDKey, HDKIndex, HDKIndexType, Invitation, nip19Extension, Nip76Wallet, INostrNip76Provider, + Nip76WebWalletStorage, NostrEventDocument, NostrKinds, PostDocument, PrivateChannel, Rsvp, Versions, walletRsvpDocumentsOffset } from 'animiq-nip76-tools'; import * as nostrTools from 'nostr-tools'; import { filter, firstValueFrom, Subject, take } from 'rxjs'; @@ -56,33 +54,21 @@ export class Nip76Service { }); } - get extensionProvider(): any { + get extensionProvider(): INostrNip76Provider { return (globalThis as any).nostr?.nip76; } async loadWallet() { const publicKey = this.profile.pubkey; if (this.extensionProvider) { - const args = await this.extensionProvider.getWalletArgs(); - const rootKey = HDKey.parseExtendedKey(args.rootKey); - const wordset = Uint32Array.from(args.wordset); - const documentsIndex: HDKIndex = HDKIndex.fromJSON(args.documentsIndex); - const walletArgs: Nip76WalletConstructorArgs = { - publicKey: args.publicKey, - wordset, - rootKey, - documentsIndex, - store: {} as Nip76WebWalletStorage, - isGuest: false, - isInSession: true, - }; - this.wallet = new Nip76Wallet(walletArgs); + const documentsIndex: HDKIndex = await this.extensionProvider.getIndex(); + this.wallet = new Nip76Wallet({ publicKey, documentsIndex }); setTimeout(() => { this.loadDocuments(); }, 500); } else if (localStorage.getItem(nostrPrivKeyAddress)) { if (localStorage.getItem(Nip76WebWalletStorage.backupKey)) { Nip76WebWalletStorage.fromStorage({ publicKey }).then(wallet => { this.wallet = wallet; - if (this.wallet.isInSession) { + if (this.wallet.isReady) { setTimeout(() => { this.loadDocuments(); }, 500); } else if (!this.wallet.isGuest) { this.login(); @@ -130,10 +116,10 @@ export class Nip76Service { async login(): Promise { const privateKey = await this.passwordDialog('Load Private Channel Keys'); this.wallet = await Nip76WebWalletStorage.fromStorage({ privateKey }); - if (this.wallet.isInSession) { + if (this.wallet.isReady) { this.loadDocuments(); } - return this.wallet.isInSession; + return this.wallet.isReady; } async logout() { @@ -147,8 +133,8 @@ export class Nip76Service { } loadDocuments(start = 0) { - const channelPubkeys = this.wallet.documentsIndex.getSequentialKeyset(0, 0); - const invitePubkeys = this.wallet.documentsIndex.getSequentialKeyset(walletRsvpDocumentsOffset, 0); + const channelPubkeys = this.wallet.documentsIndex!.getSequentialKeyset(0, 0); + const invitePubkeys = this.wallet.documentsIndex!.getSequentialKeyset(walletRsvpDocumentsOffset, 0); if (this.documentsSubscription) { this.relayService.unsubscribe(this.documentsSubscription.id); } @@ -156,18 +142,18 @@ export class Nip76Service { privateDoc$.subscribe(async nostrEvent => { let docIndex = channelPubkeys.keys.findIndex(x => x.signingKey?.nostrPubKey === nostrEvent.pubkey) + start; if (docIndex > -1) { - const doc = await this.wallet.documentsIndex.readEvent(nostrEvent, docIndex) as PrivateChannel; - if(this.extensionProvider) { - const dkxInviteDTO = await this.extensionProvider.getInvitationIndex(docIndex); - doc.dkxInvite = HDKIndex.fromJSON(dkxInviteDTO); + const doc = await this.wallet.documentsIndex!.readEvent(nostrEvent, docIndex) as PrivateChannel; + if (this.extensionProvider) { + doc.dkxInvite = await this.extensionProvider.getIndex(docIndex); } + doc.dkxInvite.parentDocument = doc; if (doc) { this.loadChannel(doc); } } else { docIndex = invitePubkeys.keys.findIndex(x => x.signingKey?.nostrPubKey === nostrEvent.pubkey) + start + walletRsvpDocumentsOffset; - const doc = await this.wallet.documentsIndex.readEvent(nostrEvent, docIndex); - if (doc && doc instanceof Rsvp) { + const doc = await this.wallet.documentsIndex!.readEvent(nostrEvent, docIndex) as Rsvp; + if (doc) { const pointer: nip19Extension.PrivateChannelPointer = { type: doc.content.type, docIndex: doc.content.pointerDocIndex, @@ -284,15 +270,15 @@ export class Nip76Service { const channel = await channelIndex.readEvent(nostrEvent) as PrivateChannel; if (channel) { channel.invitation = invite; - const exisitng = this.wallet.documentsIndex.documents.find(x => x.nostrEvent.id === nostrEvent.id) as PrivateChannel; + const exisitng = this.wallet.documentsIndex!.documents.find(x => x.nostrEvent.id === nostrEvent.id) as PrivateChannel; if (exisitng) { channel.dkxPost.documents = exisitng.dkxPost.documents; channel.dkxRsvp.documents = exisitng.dkxRsvp.documents; channel.dkxInvite = exisitng.dkxInvite; - const index = this.wallet.documentsIndex.documents.findIndex(x => x.nostrEvent.id === nostrEvent.id); - this.wallet.documentsIndex.documents[index] = channel; + const index = this.wallet.documentsIndex!.documents.findIndex(x => x.nostrEvent.id === nostrEvent.id); + this.wallet.documentsIndex!.documents[index] = channel; } else { - this.wallet.documentsIndex.documents.push(channel); + this.wallet.documentsIndex!.documents.push(channel); } this.loadChannel(channel); return channel; @@ -313,8 +299,7 @@ export class Nip76Service { if ((pointerType & nip19Extension.PointerType.SharedSecret) == nip19Extension.PointerType.SharedSecret) { if (this.extensionProvider) { - const pointerDTO = await this.extensionProvider.readInvitation(channelPointer); - pointer = nip19Extension.pointerFromDTO(pointerDTO); + pointer = await this.extensionProvider.readInvitation(channelPointer); } else { secret = await this.passwordDialog('Preview Private Invitation'); const p = await nip19Extension.decode(channelPointer, secret!); @@ -377,13 +362,14 @@ export class Nip76Service { } async saveChannel(channel: PrivateChannel, privateKey?: string) { + channel.dkxParent = this.wallet.documentsIndex!; channel.content.created_at = channel.content.created_at || channel?.nostrEvent.created_at || getNowSeconds(); let event: NostrEventDocument; if (this.extensionProvider) { - event = await this.extensionProvider.createEvent(this.wallet.documentsIndex, channel); + event = await this.extensionProvider.createEvent(channel); } else { privateKey = privateKey || await this.passwordDialog('Save Channel Details'); - event = await this.wallet.documentsIndex.createEvent(channel, privateKey); + event = await this.wallet.documentsIndex!.createEvent(channel, privateKey); } await this.dataService.publishEvent(event); return true; @@ -391,6 +377,7 @@ export class Nip76Service { async saveNote(channel: PrivateChannel, text: string) { const postDocument = new PostDocument(); + postDocument.dkxParent = channel.dkxPost; postDocument.content = { text, pubkey: this.wallet.ownerPubKey, @@ -398,7 +385,7 @@ export class Nip76Service { } let event: NostrEventDocument; if (this.extensionProvider) { - event = await this.extensionProvider.createEvent(channel.dkxPost, postDocument); + event = await this.extensionProvider.createEvent(postDocument); } else { const privateKey = await this.passwordDialog('Save Note'); event = await channel.dkxPost.createEvent(postDocument, privateKey); @@ -409,6 +396,7 @@ export class Nip76Service { async saveReaction(post: PostDocument, text: string, kind: nostrTools.Kind): Promise { const postDocument = new PostDocument(); + postDocument.dkxParent = post.dkxParent; postDocument.content = { kind, pubkey: this.wallet.ownerPubKey, @@ -417,7 +405,7 @@ export class Nip76Service { }; let event: NostrEventDocument; if (this.extensionProvider) { - event = await this.extensionProvider.createEvent(post.dkxParent, postDocument); + event = await this.extensionProvider.createEvent(postDocument); } else { const privateKey = await this.passwordDialog('Save ' + (kind === nostrTools.Kind.Reaction ? 'Reaction' : 'Reply')); event = await post.dkxParent.createEvent(postDocument, privateKey); @@ -428,6 +416,7 @@ export class Nip76Service { async saveInvitation(channel: PrivateChannel, invitation: AddInvitationDialogData): Promise { const invite = new Invitation(); + invite.dkxParent = channel.dkxInvite; invite.docIndex = channel.dkxInvite.documents.length + 1; invite.content = { kind: NostrKinds.PrivateChannelInvitation, @@ -440,7 +429,7 @@ export class Nip76Service { }; let event: NostrEventDocument; if (this.extensionProvider) { - event = await this.extensionProvider.createEvent(channel.dkxInvite, invite); + event = await this.extensionProvider.createEvent(invite); } else { const privateKey = await this.passwordDialog('Save Invitation'); event = await channel.dkxInvite.createEvent(invite, privateKey); @@ -450,11 +439,12 @@ export class Nip76Service { } async resaveInvitation(channel: PrivateChannel, invite: Invitation, withKeys: boolean): Promise { + invite.dkxParent = channel.dkxInvite; invite.content.signingParent = withKeys ? channel.dkxPost.signingParent : undefined; invite.content.encryptParent = withKeys ? channel.dkxPost.encryptParent : undefined; let event: NostrEventDocument; if (this.extensionProvider) { - event = await this.extensionProvider.createEvent(channel.dkxInvite, invite); + event = await this.extensionProvider.createEvent(invite); } else { const privateKey = await this.passwordDialog((withKeys ? 'Reinstate' : 'Suspend') + ' Invitation'); event = await channel.dkxInvite.createEvent(invite, privateKey); @@ -465,6 +455,7 @@ export class Nip76Service { async saveRSVP(channel: PrivateChannel) { const rsvp = new Rsvp(); + rsvp.dkxParent = channel.dkxRsvp; rsvp.content = { kind: NostrKinds.PrivateChannelRSVP, pubkey: this.wallet.ownerPubKey, @@ -474,21 +465,22 @@ export class Nip76Service { let event1: NostrEventDocument; let privateKey: string; if (this.extensionProvider) { - event1 = await this.extensionProvider.createEvent(channel.dkxRsvp, rsvp); + event1 = await this.extensionProvider.createEvent(rsvp); } else { privateKey = await this.passwordDialog('Save RSVP'); event1 = await channel.dkxRsvp.createEvent(rsvp, privateKey); } await this.dataService.publishEvent(event1); + rsvp.dkxParent = this.wallet.documentsIndex!; rsvp.docIndex = channel.invitation.docIndex || (this.wallet.rsvps.length + 1 + walletRsvpDocumentsOffset); rsvp.content.signingKey = channel.invitation.pointer.signingKey; rsvp.content.cryptoKey = channel.invitation.pointer.cryptoKey; let event2: NostrEventDocument; if (this.extensionProvider) { - event2 = await this.extensionProvider.createEvent(this.wallet.documentsIndex, rsvp); + event2 = await this.extensionProvider.createEvent(rsvp); } else { - event2 = await this.wallet.documentsIndex.createEvent(rsvp, privateKey!); + event2 = await this.wallet.documentsIndex!.createEvent(rsvp, privateKey!); } await this.dataService.publishEvent(event2);