diff --git a/src/app/app.html b/src/app/app.html index 8407442..1b7281e 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -87,14 +87,15 @@ notifications {{ 'App.Notifications' | translate }} - + people {{ 'App.People' | translate }} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 7b254da..940ec48 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -154,6 +154,7 @@ import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; import { DragScrollModule } from 'ngx-drag-scroll'; import { ZappersListDialogComponent } from './shared/zappers-list-dialog/zappers-list-dialog.component'; import { ExampleComponent } from './example/example'; +import { MessageListComponent } from './shared/message-list/message-list.component'; @NgModule({ declarations: [ AppComponent, @@ -245,7 +246,8 @@ import { ExampleComponent } from './example/example'; TagsComponent, BadgeComponent, ZappersListDialogComponent, - ExampleComponent + ExampleComponent, + MessageListComponent ], imports: [ HttpClientModule, diff --git a/src/app/chat/chat.html b/src/app/chat/chat.html index 1811959..0364b00 100644 --- a/src/app/chat/chat.html +++ b/src/app/chat/chat.html @@ -1,4 +1,47 @@ - + + + + + + +

{{ chat.name }}

+

{{ chat.about }}

+ +
+ + +
+ + +
+ + + + + + + + + + + + diff --git a/src/app/chat/chat.scss b/src/app/chat/chat.scss index 16fab21..3ebeb3a 100644 --- a/src/app/chat/chat.scss +++ b/src/app/chat/chat.scss @@ -29,3 +29,20 @@ .spacer { flex: 1; } + + +.form { + padding: 16px 16px 0 16px; +} + +.input-full-width { + position: relative; + margin: auto; +} + +.search { + position: sticky; + top: 0; + padding: 10px; + z-index: 999; +} diff --git a/src/app/chat/chat.ts b/src/app/chat/chat.ts index 966c1a9..f85269f 100644 --- a/src/app/chat/chat.ts +++ b/src/app/chat/chat.ts @@ -1,6 +1,10 @@ import { Component, ChangeDetectorRef, ViewChild, ViewEncapsulation } from '@angular/core'; import { MatSidenav } from '@angular/material/sidenav'; import { ApplicationState } from '../services/applicationstate'; +import { ChatService } from '../services/chat.service'; +import { RelayService } from '../services/relay'; +import { Kind } from 'nostr-tools'; +import { UIService } from '../services/ui'; @Component({ selector: 'app-chat', templateUrl: './chat.html', @@ -10,7 +14,10 @@ import { ApplicationState } from '../services/applicationstate'; export class ChatComponent { @ViewChild('chatSidebar', { static: false }) chatSidebar!: MatSidenav; @ViewChild('userSidebar', { static: false }) userSidebar!: MatSidenav; - constructor(private appState: ApplicationState) {} + + subscription: any; + + constructor(private appState: ApplicationState, private chatService: ChatService, private relayService: RelayService, public ui: UIService) {} sidebarTitles = { user: '', chat: '', @@ -29,8 +36,13 @@ export class ChatComponent { }; async ngOnInit() { + this.ui.clearChats(); + this.appState.updateTitle('Chat'); this.appState.goBack = true; this.appState.actions = []; + + this.subscription = this.relayService.subscribe([{ kinds: [Kind.ChannelCreation, Kind.ChannelMetadata], limit: 10 }]).id; + // this.chatService.downloadChatRooms(); } } diff --git a/src/app/services/chat.service.ts b/src/app/services/chat.service.ts index 3a3d689..2167367 100644 --- a/src/app/services/chat.service.ts +++ b/src/app/services/chat.service.ts @@ -51,6 +51,42 @@ export class ChatService { subscriptions: Subscription[] = []; + downloadChatRooms() { + debugger; + // this.chats2 = []; + this.#chats = []; + + this.dataService + .downloadEventsByQuery([{ kinds: [40, 41] }], 3000) + .pipe( + finalize(async () => { + debugger; + for (let index = 0; index < this.#chats.length; index++) { + const event = this.#chats[index]; + const content = await this.nostr.decrypt(event.pubkey, event.content); + event.content = content; + console.log('DECRYPTED EVENT:', event); + } + }) + ) + .subscribe(async (event) => { + if (this.#chats.findIndex((e) => e.id === event.id) > -1) { + return; + } + + // const gt = globalThis as any; + // const content = await gt.nostr.nip04.decrypt(event.pubkey, event.content); + // event.content = content; + + this.#chats.unshift(event); + + // this.chats2.push(event); + // this.#chatsChanged2.next(this.chats2); + }); + + // this.subscriptions.push(this.dataService.downloadEventsByQuery([{}])); + } + download() { // this.chats2 = []; this.#chats = []; diff --git a/src/app/services/data.ts b/src/app/services/data.ts index 9acdb18..10b00c9 100644 --- a/src/app/services/data.ts +++ b/src/app/services/data.ts @@ -582,7 +582,6 @@ export class DataService { downloadFromRelay(filters: Filter[], relay: NostrRelay, requestTimeout = 10000): Observable { return new Observable((observer: Observer) => { const sub = relay.sub([...filters], {}) as NostrSubscription; - // relay.subscriptions.push(sub); sub.on('event', (originalEvent: any) => { const event = this.eventService.processEvent(originalEvent); @@ -599,8 +598,6 @@ export class DataService { }); return () => { - // console.log('downloadFromRelay:finished:unsub'); - // When the observable is finished, this return function is called. sub.unsub(); }; }).pipe( @@ -608,7 +605,7 @@ export class DataService { catchError((error) => { console.warn('The observable was timed out.'); return of(); - }) // Simply return undefined when the timeout is reached. + }) ); } diff --git a/src/app/services/interfaces.ts b/src/app/services/interfaces.ts index 70459fa..42f6560 100644 --- a/src/app/services/interfaces.ts +++ b/src/app/services/interfaces.ts @@ -331,6 +331,12 @@ export interface CustomObjectModel { formatted?: string; } +export interface NostrEventChat extends NostrEvent { + about: string; + name: string; + picture: string; +} + export class ChatModel { 'id': number; 'targetUserId': number; diff --git a/src/app/services/relay.ts b/src/app/services/relay.ts index 6473cb7..afd1a02 100644 --- a/src/app/services/relay.ts +++ b/src/app/services/relay.ts @@ -427,6 +427,14 @@ export class RelayService { this.zapUi.addZap(event); } + if (event.kind == Kind.ChannelCreation) { + this.ui.putChat(event); + } + + if (event.kind == Kind.ChannelMetadata) { + this.ui.putChatMetadata(event); + } + if (response.subscription) { const sub = this.subs.get(response.subscription); if (sub) { diff --git a/src/app/services/ui.ts b/src/app/services/ui.ts index 554d6d8..8c6e6ed 100644 --- a/src/app/services/ui.ts +++ b/src/app/services/ui.ts @@ -3,7 +3,7 @@ import { ActivatedRoute } from '@angular/router'; import { Kind } from 'nostr-tools'; import { BehaviorSubject, map, Observable, filter, flatMap, mergeMap, concatMap, tap, take, single, takeWhile, from, of } from 'rxjs'; import { EventService } from './event'; -import { EmojiEnum, LoadMoreOptions, NostrEvent, NostrEventDocument, NostrProfileDocument, NotificationModel, ThreadEntry } from './interfaces'; +import { EmojiEnum, LoadMoreOptions, NostrEvent, NostrEventChat, NostrEventDocument, NostrProfileDocument, NotificationModel, ThreadEntry } from './interfaces'; import { OptionsService } from './options'; import { ProfileService } from './profile'; import { ZapService } from './zap.service'; @@ -26,6 +26,7 @@ export class UIService { rootEventsView: [] as NostrEventDocument[], replyEventsView: [] as NostrEventDocument[], reactions: new Map(), + chats: [] as NostrEventChat[], }; viewCounts = { @@ -156,6 +157,14 @@ export class UIService { // return this.#eventsChanged.asObservable().pipe(map((data) => data.sort((a, b) => (a.created_at > b.created_at ? -1 : 1)))); } + chats: NostrEventChat[] = []; + + #chatsChanged: BehaviorSubject = new BehaviorSubject(this.chats); + + get chats$(): Observable { + return this.#chatsChanged.asObservable(); + } + #loadMore: BehaviorSubject = new BehaviorSubject(undefined); get loadMore$(): Observable { @@ -285,6 +294,69 @@ export class UIService { this.triggerUnreadNotifications(); } + putChat(event: NostrEvent) { + const index = this.chats.findIndex((n) => n.id == event.id); + + if (index == -1) { + const chat = event as NostrEventChat; + const parsed = JSON.parse(chat.content); + + chat.picture = parsed.picture; + chat.name = parsed.name; + chat.about = parsed.about; + + this.chats.push(chat); + this.#chatsChanged.next(this.chats); + } + + // if (index == -1) { + // this.#notifications.unshift(notification); + + // this.#notifications = this.#notifications.sort((a, b) => { + // return a.created < b.created ? 1 : -1; + // }); + // } else { + // this.#notifications[index] = notification; + // } + + // this.#activityFeed = this.#notifications.slice(0, 5); + // this.triggerUnreadNotifications(); + } + + putChatMetadata(event: NostrEvent) { + const channelId = this.eventService.lastETag(event); + + if (!channelId) { + console.debug('This channel metadata does not have eTag:', event); + return; + } + + // Find the existing chat creation, but verify both channel ID and the public key. + const index = this.chats.findIndex((n) => n.id == channelId && n.pubkey == event.pubkey); + + // TODO: We are subscribing to both 40 and 41 at the same time and we are receiving 41 (metadata updates) + // before some of the 40 (create) events, meaning we'll never show the latest metadata for certain chats. + if (index == -1) { + return; + } + + this.chats[index].content = event.content; + this.#chatsChanged.next(this.chats); + + // if (index == -1) { + // this.#notifications.unshift(notification); + + // this.#notifications = this.#notifications.sort((a, b) => { + // return a.created < b.created ? 1 : -1; + // }); + // } else { + // this.#notifications[index] = notification; + // } + + // this.#activityFeed = this.#notifications.slice(0, 5); + // this.triggerUnreadNotifications(); + } + viewEventsStart = 0; viewEventsCount = 5; @@ -709,6 +781,7 @@ export class UIService { this.#lists.followingEventsView = []; this.#lists.reactions = new Map(); + this.#lists.chats = []; this.#notifications = []; this.#activityFeed = []; @@ -766,6 +839,10 @@ export class UIService { this.previousFeedSinceValue = 0; } + clearChats() { + this.#lists.chats = []; + } + // #parentEventId: string | undefined = undefined; // get parentEventId() { diff --git a/src/app/shared/chat-item/chat-item.component.ts b/src/app/shared/chat-item/chat-item.component.ts index e2a529d..6a2cd92 100644 --- a/src/app/shared/chat-item/chat-item.component.ts +++ b/src/app/shared/chat-item/chat-item.component.ts @@ -9,7 +9,7 @@ import { ChatModel, NostrEventDocument } from 'src/app/services/interfaces'; }) export class ChatItemComponent { @Output() openChatSidebar: EventEmitter = new EventEmitter(); - @Input() chat!: ChatModel; + @Input() chat!: ChatModel | any; @Input() event!: NostrEventDocument; constructor(private service: ChatService) {} diff --git a/src/app/shared/chat-list/chat-list.component.ts b/src/app/shared/chat-list/chat-list.component.ts index 3d3124f..9c4dc83 100644 --- a/src/app/shared/chat-list/chat-list.component.ts +++ b/src/app/shared/chat-list/chat-list.component.ts @@ -18,8 +18,7 @@ export class ChatListComponent implements OnInit { constructor(public chatService: ChatService) {} ngOnInit() { - this.chatService.download(); - + // this.chatService.download(); // this.chatService.uniqueChats$.subscribe((data) => { // console.log('YEEH!', data); // }); diff --git a/src/app/shared/message-list/message-list.component.html b/src/app/shared/message-list/message-list.component.html new file mode 100644 index 0000000..dc9188b --- /dev/null +++ b/src/app/shared/message-list/message-list.component.html @@ -0,0 +1,35 @@ + + + + + + + + + + + + +
{{ chat.pubkey }} : {{ chat.content }}
+ + + + + + + + + + diff --git a/src/app/shared/message-list/message-list.component.scss b/src/app/shared/message-list/message-list.component.scss new file mode 100644 index 0000000..42faf70 --- /dev/null +++ b/src/app/shared/message-list/message-list.component.scss @@ -0,0 +1,15 @@ +.form { + padding: 16px 16px 0 16px; +} + +.input-full-width { + position: relative; + margin: auto; +} + +.search { + position: sticky; + top: 0; + padding: 10px; + z-index: 999; +} diff --git a/src/app/shared/message-list/message-list.component.spec.ts b/src/app/shared/message-list/message-list.component.spec.ts new file mode 100644 index 0000000..5f7894e --- /dev/null +++ b/src/app/shared/message-list/message-list.component.spec.ts @@ -0,0 +1,25 @@ +// import {async, ComponentFixture, TestBed} from '@angular/core/testing'; + +// import {ChatListComponent} from './chat-list.component'; + +// describe('ChatListComponent', () => { +// let component: ChatListComponent; +// let fixture: ComponentFixture; + +// beforeEach(async(() => { +// TestBed.configureTestingModule({ +// declarations: [ChatListComponent] +// }) +// .compileComponents(); +// })); + +// beforeEach(() => { +// fixture = TestBed.createComponent(ChatListComponent); +// component = fixture.componentInstance; +// fixture.detectChanges(); +// }); + +// it('should create', () => { +// expect(component).toBeTruthy(); +// }); +// }); diff --git a/src/app/shared/message-list/message-list.component.ts b/src/app/shared/message-list/message-list.component.ts new file mode 100644 index 0000000..ebfad26 --- /dev/null +++ b/src/app/shared/message-list/message-list.component.ts @@ -0,0 +1,35 @@ +import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; +import { from, Observable, of } from 'rxjs'; +import { ChatService } from 'src/app/services/chat.service'; + +interface ChatModel { + id: string; + name: string; +} + +@Component({ + selector: 'app-message-list', + templateUrl: './message-list.component.html', + styleUrls: ['./message-list.component.scss'], +}) +export class MessageListComponent implements OnInit { + @Output() openChatSidebar: EventEmitter = new EventEmitter(); + + constructor(public chatService: ChatService) {} + + ngOnInit() { + this.chatService.download(); + + // this.chatService.uniqueChats$.subscribe((data) => { + // console.log('YEEH!', data); + // }); + } + + add() { + // this.#chats.unshift({ id: '123', name: 'Yes!' }); + } + + reset() { + // this.#chats = []; + } +}