diff --git a/src/app/app.html b/src/app/app.html index 84074423..1b7281e6 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 7b254da5..940ec483 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 1811959a..b395b595 100644 --- a/src/app/chat/chat.html +++ b/src/app/chat/chat.html @@ -1,9 +1,9 @@ - + - + diff --git a/src/app/chat/chat.ts b/src/app/chat/chat.ts index 966c1a9b..bafd90d9 100644 --- a/src/app/chat/chat.ts +++ b/src/app/chat/chat.ts @@ -1,6 +1,9 @@ import { Component, ChangeDetectorRef, ViewChild, ViewEncapsulation } from '@angular/core'; import { MatSidenav } from '@angular/material/sidenav'; import { ApplicationState } from '../services/applicationstate'; +import { UIService } from '../services/ui'; +import { RelayService } from '../services/relay'; +import { Kind } from 'nostr-tools'; @Component({ selector: 'app-chat', templateUrl: './chat.html', @@ -10,7 +13,9 @@ 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?: string; + + constructor(private appState: ApplicationState, private relayService: RelayService, public ui: UIService) {} sidebarTitles = { user: '', chat: '', @@ -22,6 +27,7 @@ export class ChatComponent { this.me.sidebarTitles.user = title; }, chatSideBar: function (title: string = '') { + debugger; this.me.chatSidebar.open(); this.me.userSidebar.close(); this.me.sidebarTitles.chat = title; @@ -29,8 +35,16 @@ export class ChatComponent { }; async ngOnInit() { + this.ui.clearChats(); + this.subscription = this.relayService.subscribe([{ kinds: [Kind.ChannelCreation, Kind.ChannelMetadata], limit: 10 }]).id; + this.appState.updateTitle('Chat'); this.appState.goBack = true; this.appState.actions = []; } + + ngOnDestroy() { + // this.relayService.unsubscribe(this.subscription!); + // this.ui.clearChats(); + } } diff --git a/src/app/message/message.ts b/src/app/message/message.ts index e74787e4..7e4755ae 100644 --- a/src/app/message/message.ts +++ b/src/app/message/message.ts @@ -32,7 +32,7 @@ export class MessageComponent { }; async ngOnInit() { - this.appState.updateTitle('@Milad'); + this.appState.updateTitle(''); this.appState.goBack = true; this.appState.showBackButton = true; this.appState.actions = []; diff --git a/src/app/services/chat.service.ts b/src/app/services/chat.service.ts index 3a3d6895..21673675 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 9acdb189..10b00c92 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 70459fa4..5db1f70f 100644 --- a/src/app/services/interfaces.ts +++ b/src/app/services/interfaces.ts @@ -319,11 +319,11 @@ export interface UserModel { bio: string; } -export interface MessageModel { - id: number; - cover: string; - message: string; -} +// export interface MessageModel { +// id: number; +// cover: string; +// message: string; +// } export interface CustomObjectModel { tmpl: string; @@ -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; @@ -338,7 +344,7 @@ export class ChatModel { 'cover': string; 'lastMessage': string; 'lastMessageLength': string | number; - 'chat': Array; + // 'chat': Array; } export interface LabelModel { diff --git a/src/app/services/relay.ts b/src/app/services/relay.ts index 6473cb76..00f71083 100644 --- a/src/app/services/relay.ts +++ b/src/app/services/relay.ts @@ -427,6 +427,18 @@ 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 (event.kind == Kind.ChannelMessage) { + this.ui.putChatMessage(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 554d6d87..ce0fff43 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,8 @@ export class UIService { rootEventsView: [] as NostrEventDocument[], replyEventsView: [] as NostrEventDocument[], reactions: new Map(), + chats: [] as NostrEventChat[], + chatMessages: [] as NostrEventChat[], }; viewCounts = { @@ -156,6 +158,22 @@ 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(); + } + + chatMessage: NostrEvent[] = []; + + #chatMessageChanged: BehaviorSubject = new BehaviorSubject(this.chatMessage); + + get chatMessage$(): Observable { + return this.#chatMessageChanged.asObservable(); + } + #loadMore: BehaviorSubject = new BehaviorSubject(undefined); get loadMore$(): Observable { @@ -285,6 +303,102 @@ 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; + + try { + 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); + } catch (err) { + console.debug('Failed to parse: ', chat.content); + } + } + + // 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(); + } + + putChatMessage(event: NostrEvent) { + const index = this.chatMessage.findIndex((n) => n.id == event.id); + + if (index == -1) { + const chat = event as NostrEvent; + + try { + this.chatMessage.push(chat); + this.#chatMessageChanged.next(this.chatMessage); + } catch (err) { + console.debug('Failed to parse: ', chat.content); + } + } + + // 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 +823,8 @@ export class UIService { this.#lists.followingEventsView = []; this.#lists.reactions = new Map(); + this.#lists.chats = []; + this.#lists.chatMessages = []; this.#notifications = []; this.#activityFeed = []; @@ -766,6 +882,18 @@ export class UIService { this.previousFeedSinceValue = 0; } + clearChats() { + this.#lists.chats = []; + this.chats = []; + this.#chatsChanged.next(this.chats); + } + + clearChatMessages() { + this.#lists.chatMessages = []; + this.chatMessage = []; + this.#chatMessageChanged.next(this.chatMessage); + } + // #parentEventId: string | undefined = undefined; // get parentEventId() { diff --git a/src/app/shared/chat-detail/chat-detail.component.html b/src/app/shared/chat-detail/chat-detail.component.html index 8747665f..a70bfd7c 100644 --- a/src/app/shared/chat-detail/chat-detail.component.html +++ b/src/app/shared/chat-detail/chat-detail.component.html @@ -1,5 +1,3 @@ - - @@ -8,8 +6,9 @@
- - + + + @@ -17,15 +16,14 @@
- + - + - + attach_file_add - - Length : {{message?.length}} - sentiment_satisfied + + Length : {{ message?.length }} + sentiment_satisfied mic send @@ -53,10 +49,4 @@
-
- - - - - diff --git a/src/app/shared/chat-detail/chat-detail.component.ts b/src/app/shared/chat-detail/chat-detail.component.ts index 37a1ae12..9f43e838 100644 --- a/src/app/shared/chat-detail/chat-detail.component.ts +++ b/src/app/shared/chat-detail/chat-detail.component.ts @@ -1,10 +1,15 @@ import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { MatSidenav } from '@angular/material/sidenav'; +import { ActivatedRoute } from '@angular/router'; +import { Kind } from 'nostr-tools'; import { Subscription } from 'rxjs'; import { ApplicationState } from 'src/app/services/applicationstate'; import { ChatService } from 'src/app/services/chat.service'; import { ChatModel } from 'src/app/services/interfaces'; import { MessageControlService } from 'src/app/services/message-control.service'; +import { RelayService } from 'src/app/services/relay'; +import { UIService } from 'src/app/services/ui'; +import { Utilities } from 'src/app/services/utilities'; @Component({ selector: 'app-chat-detail', @@ -16,16 +21,45 @@ export class ChatDetailComponent implements OnInit, OnDestroy { @ViewChild('picker') picker: unknown; isEmojiPickerVisible: boolean | undefined; - subscription!: Subscription; chat!: ChatModel; sending: boolean = false; message: any; displayList = true; - constructor(private service: ChatService, private control: MessageControlService, private appState: ApplicationState) { } + constructor(private relayService: RelayService, public ui: UIService, private service: ChatService, private activatedRoute: ActivatedRoute, private utilities: Utilities, private control: MessageControlService, private appState: ApplicationState) {} @ViewChild('drawer') drawer!: MatSidenav; + subscription?: string; + subscriptions: Subscription[] = []; ngOnInit() { + this.subscriptions.push( + this.activatedRoute.paramMap.subscribe(async (params) => { + const id: any = params.get('id'); + + this.ui.clearChatMessages(); + this.relayService.unsubscribe(this.subscription!); + this.subscription = this.relayService.subscribe([{ kinds: [Kind.ChannelMessage, Kind.ChannelMuteUser, Kind.ChannelHideMessage], ['#e']: [id], limit: 500 }]).id; + + // this.ui.clearFeed(); + + // if (circle != null) { + // this.circle = Number(circle); + // this.ui.setFeedCircle(this.circle); + // } else { + // this.circle = -1; + // this.ui.setFeedCircle(this.circle); + // } + + // this.subscriptions.push( + // this.navigation.showMore$.subscribe(() => { + // this.showMore(); + // }) + // ); + }) + ); + + // debugger; + // this.subscription = this.relayService.subscribe([{ kinds: [Kind.ChannelMessage, Kind.ChannelMuteUser, Kind.ChannelHideMessage], ['#e']: [this.pubkey], limit: 500 }]).id; // this.subscription = this.service.chat.subscribe((messages) => { // this.chat = messages; @@ -72,6 +106,7 @@ export class ChatDetailComponent implements OnInit, OnDestroy { } ngOnDestroy(): void { - this.subscription?.unsubscribe(); + this.relayService.unsubscribe(this.subscription!); + this.utilities.unsubscribe(this.subscriptions); } } diff --git a/src/app/shared/chat-item/chat-item.component.html b/src/app/shared/chat-item/chat-item.component.html index 7b081d97..2e71be86 100644 --- a/src/app/shared/chat-item/chat-item.component.html +++ b/src/app/shared/chat-item/chat-item.component.html @@ -1,6 +1,11 @@ - + + {{ chat.name }} + {{ chat.about }} + + + + diff --git a/src/app/shared/chat-item/chat-item.component.ts b/src/app/shared/chat-item/chat-item.component.ts index e2a529df..d4137542 100644 --- a/src/app/shared/chat-item/chat-item.component.ts +++ b/src/app/shared/chat-item/chat-item.component.ts @@ -9,13 +9,13 @@ 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) {} showMessageDetail() { - this.openChatSidebar.emit(this.chat.username); + this.openChatSidebar.emit(this.chat.id); // this.service.chat.next(this.chat); } } diff --git a/src/app/shared/chat-list/chat-list.component.html b/src/app/shared/chat-list/chat-list.component.html index dc9188b7..8f8e4f86 100644 --- a/src/app/shared/chat-list/chat-list.component.html +++ b/src/app/shared/chat-list/chat-list.component.html @@ -7,7 +7,7 @@ - + @@ -18,7 +18,7 @@ --> -
{{ chat.pubkey }} : {{ chat.content }}
+ - + diff --git a/src/app/shared/chat-list/chat-list.component.ts b/src/app/shared/chat-list/chat-list.component.ts index 3d3124f9..f5f34864 100644 --- a/src/app/shared/chat-list/chat-list.component.ts +++ b/src/app/shared/chat-list/chat-list.component.ts @@ -1,6 +1,9 @@ import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; +import { Kind } from 'nostr-tools'; import { from, Observable, of } from 'rxjs'; import { ChatService } from 'src/app/services/chat.service'; +import { RelayService } from 'src/app/services/relay'; +import { UIService } from 'src/app/services/ui'; interface ChatModel { id: string; @@ -15,16 +18,21 @@ interface ChatModel { export class ChatListComponent implements OnInit { @Output() openChatSidebar: EventEmitter = new EventEmitter(); - constructor(public chatService: ChatService) {} + constructor(public chatService: ChatService, public ui: UIService, private relayService: RelayService) {} ngOnInit() { - this.chatService.download(); + + // this.chatService.download(); // this.chatService.uniqueChats$.subscribe((data) => { // console.log('YEEH!', data); // }); } + ngOnDestroy() { + + } + add() { // this.#chats.unshift({ id: '123', name: 'Yes!' }); } diff --git a/src/app/shared/message-bubble/message-bubble.component.html b/src/app/shared/message-bubble/message-bubble.component.html index f2307403..c5cdb477 100644 --- a/src/app/shared/message-bubble/message-bubble.component.html +++ b/src/app/shared/message-bubble/message-bubble.component.html @@ -1,10 +1,11 @@ -
+
- + +
- {{message.message}} + {{ message.content }}
diff --git a/src/app/shared/message-bubble/message-bubble.component.ts b/src/app/shared/message-bubble/message-bubble.component.ts index fe62c9c0..e03b725e 100644 --- a/src/app/shared/message-bubble/message-bubble.component.ts +++ b/src/app/shared/message-bubble/message-bubble.component.ts @@ -1,13 +1,20 @@ -import {Component, Input} from '@angular/core'; -import { MessageModel } from 'src/app/services/interfaces'; - +import { Component, Input } from '@angular/core'; +import { ApplicationState } from 'src/app/services/applicationstate'; +import { NostrEvent } from 'src/app/services/interfaces'; @Component({ selector: 'app-message-bubble', templateUrl: './message-bubble.component.html', - styleUrls: ['./message-bubble.component.scss'] + styleUrls: ['./message-bubble.component.scss'], }) export class MessageBubbleComponent { - @Input() message!: MessageModel; + @Input() message!: NostrEvent; @Input() cover!: string; + me?: string; + + constructor(private appState: ApplicationState) {} + + ngOnInit() { + this.me = this.appState.getPublicKey(); + } } 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 00000000..dc9188b7 --- /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 00000000..42faf705 --- /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 00000000..5f7894e2 --- /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 00000000..ebfad266 --- /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 = []; + } +}