Compare commits

...

8 Commits

Author SHA1 Message Date
SondreB
7fe89d8969
Merge c88de0d3e3 into 4373fe0346 2024-06-13 18:40:32 +00:00
vr-varad
4373fe0346
Added default banner for profile (#145)
Co-authored-by: vr-varad <varadgupta21#gmail.com>
2024-04-22 13:30:09 +02:00
Harshil Jani
09dd203b43
Add ability to follow from event actions (#143)
Signed-off-by: Harshil-Jani <harshiljani2002@gmail.com>
2024-04-16 22:46:46 +02:00
SondreB
5d91211754
Add persistence of metrics and sort by popular on home (#134)
- Closes #101
2024-04-09 10:08:36 +02:00
Harshil Jani
17be82375e
Allow tagging(mention) in Notes with username or display name (#137)
Signed-off-by: Harshil-Jani <harshiljani2002@gmail.com>
2024-04-09 10:04:58 +02:00
SondreB
c88de0d3e3
Add very basic chat display 2023-05-28 21:44:32 +02:00
SondreB
f3e7422c1a
Improve the listing of public chats 2023-05-28 20:56:59 +02:00
SondreB
a29dc1100c
Add display of a limited set of public chats 2023-05-28 20:49:37 +02:00
29 changed files with 470 additions and 74 deletions

View File

@ -87,14 +87,15 @@
<mat-icon [matBadgeHidden]="(ui.unreadNotifications$ | async) == 0" [matBadge]="ui.unreadNotifications$ | async">notifications</mat-icon>
<span *ngIf="displayLabels">{{ 'App.Notifications' | translate }}</span>
</a>
<!-- <a [routerLink]="['/chat']" mat-menu-item (click)="toggleMenu()" [routerLinkActiveOptions]="{ exact: true }" routerLinkActive="active">
<a [routerLink]="['/chat']" mat-menu-item (click)="toggleMenu()" [routerLinkActiveOptions]="{ exact: true }" routerLinkActive="active">
<mat-icon>chat</mat-icon>
<span *ngIf="displayLabels">Channels</span>
<span *ngIf="displayLabels">Chats</span>
</a>
<a [routerLink]="['/m']" mat-menu-item (click)="toggleMenu()" [routerLinkActiveOptions]="{ exact: true }" routerLinkActive="active">
<mat-icon matBadge="2">mail</mat-icon>
<span *ngIf="displayLabels">Messages</span>
</a> -->
</a>
<a [routerLink]="['/people']" mat-menu-item (click)="toggleMenu()" [routerLinkActiveOptions]="{ exact: true }" routerLinkActive="active">
<mat-icon>people</mat-icon>
<span *ngIf="displayLabels">{{ 'App.People' | translate }}</span>

View File

@ -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,

View File

@ -1,9 +1,9 @@
<mat-sidenav-container class="drawer-container" autosize>
<mat-tab-group mat-align-tabs="center">
<mat-tab label="Messages">
<mat-tab label="Subscribed">
<app-chat-list (openChatSidebar)="open.chatSideBar($event)"></app-chat-list>
</mat-tab>
<mat-tab label="Users">
<mat-tab label="Public">
<app-user-list (openUserSidebar)="open.userSideBar($event)"></app-user-list>
</mat-tab>
</mat-tab-group>

View File

@ -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();
}
}

View File

@ -17,6 +17,7 @@ import { DataService } from '../services/data';
import { StorageService } from '../services/storage';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { UIService } from '../services/ui';
import { MetricService } from '../services/metric-service';
interface DefaultProfile {
pubkey: string;
@ -118,7 +119,8 @@ export class HomeComponent {
private router: Router,
private breakpointObserver: BreakpointObserver,
private ngZone: NgZone,
private formBuilder: UntypedFormBuilder
private formBuilder: UntypedFormBuilder,
private metricService: MetricService
) {
console.log('HOME constructor!!'); // Hm.. called twice, why?
}
@ -257,7 +259,11 @@ export class HomeComponent {
this.profileService.following$.subscribe((profiles) => {
// this.profileCount = Math.floor(window.innerWidth / this.profileThumbnailWidth);
this.profileCount = 75;
this.profiles = profiles.slice(0, this.profileCount);
let slized = profiles.slice(0, this.profileCount);
let sorted = slized.sort((a, b) => {
return this.metricService.get(a.pubkey) < this.metricService.get(b.pubkey) ? 1 : -1;
});
this.profiles = sorted;
})
);

View File

@ -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 = [];

View File

@ -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 = [];

View File

@ -582,7 +582,6 @@ export class DataService {
downloadFromRelay(filters: Filter[], relay: NostrRelay, requestTimeout = 10000): Observable<NostrEventDocument> {
return new Observable<NostrEventDocument>((observer: Observer<NostrEventDocument>) => {
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.
})
);
}

View File

@ -90,6 +90,7 @@ export interface StateDocument {
since: number;
modified?: number;
mediaQueue: MediaItem [];
metrics: { users: any }
}
export interface NostrRelayDocument {
@ -319,11 +320,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 +332,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 +345,7 @@ export class ChatModel {
'cover': string;
'lastMessage': string;
'lastMessageLength': string | number;
'chat': Array<MessageModel>;
// 'chat': Array<MessageModel>;
}
export interface LabelModel {

View File

@ -1,12 +1,19 @@
import { Injectable } from '@angular/core';
import { StorageService } from './storage';
@Injectable({
providedIn: 'root',
})
export class MetricService {
users: {
[pubKey: string]: number;
} = {};
constructor(private storage: StorageService) {}
get users(): { [pubKey: string]: number } {
if (!this.storage.state?.metrics?.users) {
this.storage.state.metrics = { users: {} };
}
return this.storage.state.metrics.users;
}
increase(value: number, pubKey: string) {
let existingMetric = this.users[pubKey];

View File

@ -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) {

View File

@ -27,7 +27,8 @@ export class StorageService {
state = {
id: 1,
since: timeAgo,
mediaQueue: []
mediaQueue: [],
metrics: { users: {} },
};
}

View File

@ -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<string, ThreadEntry>(),
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<NostrEventChat[]> = new BehaviorSubject<NostrEventChat[]>(this.chats);
get chats$(): Observable<NostrEventChat[]> {
return this.#chatsChanged.asObservable();
}
chatMessage: NostrEvent[] = [];
#chatMessageChanged: BehaviorSubject<NostrEvent[]> = new BehaviorSubject<NostrEvent[]>(this.chatMessage);
get chatMessage$(): Observable<NostrEvent[]> {
return this.#chatMessageChanged.asObservable();
}
#loadMore: BehaviorSubject<LoadMoreOptions | undefined> = new BehaviorSubject<LoadMoreOptions | undefined>(undefined);
get loadMore$(): Observable<LoadMoreOptions | undefined> {
@ -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<string, ThreadEntry>();
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() {

View File

@ -1,5 +1,3 @@
<mat-drawer-container class="chat-detail-container" autosize>
<mat-drawer #drawer class="chat-detail-sidenav list-hide-small" opened mode="side">
<app-chat-list></app-chat-list>
@ -8,8 +6,9 @@
<div class="chat-detail-sidenav-content">
<div class="messages-list-container">
<div #scrollable class="scrollable">
<ng-container *ngIf="chat">
<ng-container *ngFor="let message of chat.chat">
<ng-container>
<ng-container *ngFor="let message of ui.chatMessage$ | async">
<!-- {{ message | json }} -->
<app-message-bubble [message]="message"></app-message-bubble>
</ng-container>
</ng-container>
@ -17,15 +16,14 @@
</div>
<div class="message-send">
<emoji-mart class="emoji-picker" *ngIf="isEmojiPickerVisible" emoji="point_up" [isNative]="true" [showPreview]="false"
(emojiSelect)="addEmoji($event)" title="Choose your emoji"></emoji-mart>
<emoji-mart class="emoji-picker" *ngIf="isEmojiPickerVisible" emoji="point_up" [isNative]="true" [showPreview]="false" (emojiSelect)="addEmoji($event)" title="Choose your emoji"></emoji-mart>
<button type="button" class="list-hide-small" style="margin: 5px;" mat-icon-button (click)="toggle()">
<mat-icon *ngIf="displayList">chevron_left</mat-icon>
<mat-icon *ngIf="!displayList">chevron_right</mat-icon>
</button>
<button type="button" class="list-hide-small" style="margin: 5px" mat-icon-button (click)="toggle()">
<mat-icon *ngIf="displayList">chevron_left</mat-icon>
<mat-icon *ngIf="!displayList">chevron_right</mat-icon>
</button>
<mat-form-field class="input-full-width">
<mat-form-field class="input-full-width">
<mat-icon class="toolbar-icon" [matMenuTriggerFor]="menu" matPrefix>attach_file_add</mat-icon>
<mat-menu #menu="matMenu">
<button mat-menu-item>
@ -41,11 +39,9 @@
<span>Document</span>
</button>
</mat-menu>
<input type="text" matInput placeholder="Write a message" [(ngModel)]="message"
(keypress)="saveMessage($event)">
<mat-hint align="end" class="hint">Length : {{message?.length}}</mat-hint>
<mat-icon class="toolbar-icon" matSuffix (click)="isEmojiPickerVisible = !isEmojiPickerVisible;"
matTooltip="Insert emoji">sentiment_satisfied</mat-icon>
<input type="text" matInput placeholder="Write a message" [(ngModel)]="message" (keypress)="saveMessage($event)" />
<mat-hint align="end" class="hint">Length : {{ message?.length }}</mat-hint>
<mat-icon class="toolbar-icon" matSuffix (click)="isEmojiPickerVisible = !isEmojiPickerVisible" matTooltip="Insert emoji">sentiment_satisfied</mat-icon>
<mat-icon class="toolbar-icon" matSuffix>mic</mat-icon>
<mat-icon class="toolbar-icon" (click)="send(message)" matSuffix>send</mat-icon>
@ -53,10 +49,4 @@
</div>
</div>
</div>
</mat-drawer-container>

View File

@ -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);
}
}

View File

@ -1,6 +1,11 @@
<mat-list-item (click)="showMessageDetail()">
<img alt="" matListItemAvatar [src]="chat.cover" />
<img alt="" matListItemAvatar [src]="chat.picture" />
<span matListItemTitle>{{ chat.name }}</span>
<span matListItemLine class="last-message">{{ chat.about }}</span>
<span matListItemLine><app-profile-name [pubkey]="chat.pubkey"></app-profile-name></span>
<!-- <img alt="" matListItemAvatar [src]="chat.cover" />
<h4 mat-line>{{ chat.username }}</h4>
<p mat-line class="last-message">{{ chat.lastMessage }}</p>
<span *ngIf="chat.lastMessageLength" [matBadge]="chat.lastMessageLength" matBadgeOverlap="false" matBadgePosition="above before" matBadgeColor="warn"></span>
<p mat-line class="last-message">{{ chat.lastMessage }}</p> -->
<!-- <span *ngIf="chat.lastMessageLength" [matBadge]="chat.lastMessageLength" matBadgeOverlap="false" matBadgePosition="above before" matBadgeColor="warn"></span> -->
</mat-list-item>

View File

@ -9,13 +9,13 @@ import { ChatModel, NostrEventDocument } from 'src/app/services/interfaces';
})
export class ChatItemComponent {
@Output() openChatSidebar: EventEmitter<string> = 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);
}
}

View File

@ -7,7 +7,7 @@
</mat-form-field>
</div>
<ng-container *ngFor="let chat of chatService.chats$ | async; let lastItem = last">
<ng-container *ngFor="let chat of ui.chats$ | async; let lastItem = last">
<app-chat-item [routerLink]="['/m', chat.id]" [chat]="chat"></app-chat-item>
<mat-divider *ngIf="!lastItem"></mat-divider>
</ng-container>
@ -18,7 +18,7 @@
</ng-container> -->
</mat-nav-list>
<div *ngFor="let chat of chatService.chats3">{{ chat.pubkey }} : {{ chat.content }}</div>
<!-- <div *ngFor="let chat of chatService.chats3">{{ chat.pubkey }} : {{ chat.content }}</div> -->
<!-- <div *ngIf="chatService.uniqueChats$ | async as chats">
<div *ngFor="let chat of chats">{{ chat.pubkey }} : {{ chat.content }}</div>
@ -28,8 +28,8 @@
<div *ngFor="let chat of chats">{{ chat.id }} : {{ chat.name }}</div>
</div> -->
<button (click)="add()">Add</button>
<!-- <button (click)="add()">Add</button>
<button (click)="reset()">Reset</button>
<button (click)="chatService.download()">Download</button>
<button (click)="chatService.download()">Download</button> -->

View File

@ -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<string> = 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!' });
}

View File

@ -298,6 +298,16 @@ export class ContentComponent {
} else {
i = res.push({ safeWord: this.utilities.sanitizeUrlAndBypass(token), word: token.substring(6), token: decoded.type });
}
} else if (token.startsWith('@')) {
const username = token.substring(1);
const npub = this.profileService.following.find((follower) => follower.name === username)?.npub;
if (npub) {
const decoded = nip19.decode(npub);
const val = decoded.data as any;
i = res.push({ safeWord: this.utilities.sanitizeUrlAndBypass(token), word: val, token: decoded.type });
} else {
res[i] += token;
}
} else if (token.startsWith('http://') || token.startsWith('https://')) {
if (this.isImage(token)) {
i = res.push({ safeWord: this.utilities.sanitizeUrlAndBypass(token), word: token, token: 'image' });

View File

@ -7,6 +7,16 @@
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="follow()" *ngIf="profile?.status == 0">
<mat-icon>person_add</mat-icon>
<span>Follow</span>
</button>
<button mat-menu-item (click)="unfollow()" *ngIf="profile?.status == 1">
<mat-icon>person_remove</mat-icon>
<span>Unfollow</span>
</button>
<button mat-menu-item (click)="saveNote()" *ngIf="event && !event.saved" [matMenuTriggerFor]="labelMenu">
<mat-icon>bookmark_add</mat-icon>
<span>Bookmark</span>

View File

@ -2,7 +2,7 @@ import { Component, Input } from '@angular/core';
import { NotesService } from 'src/app/services/notes';
import { ProfileService } from 'src/app/services/profile';
import { Utilities } from 'src/app/services/utilities';
import { NostrEventDocument, NostrNoteDocument, NostrProfile, NostrProfileDocument } from '../../services/interfaces';
import { NostrEventDocument, NostrNoteDocument, NostrProfile, NostrProfileDocument, ProfileStatus } from '../../services/interfaces';
import { MatSnackBar } from '@angular/material/snack-bar';
import { copyToClipboard } from '../utilities';
import { nip19 } from 'nostr-tools';
@ -44,16 +44,23 @@ export class EventActionsComponent {
async follow(circle?: number) {
console.log('FOLLOW:', this.profile);
if(circle==null){
circle=0;
}
if (!this.profile) {
return;
}
// If not already following, add a full follow and download recent:
if (this.profile.status != 1) {
this.profile.circle = circle;
this.profile.status = 1;
await this.profileService.follow(this.profile.pubkey, circle);
// this.feedService.downloadRecent([this.profile.pubkey]);
} else {
// If we already follow but just change the circle, do a smaller operation.
this.profile.circle = circle;
await this.profileService.setCircle(this.profile.pubkey, circle);
}
}
@ -141,7 +148,7 @@ export class EventActionsComponent {
if (!this.profile) {
return;
}
this.profile.status = 0;
await this.profileService.unfollow(this.profile.pubkey);
}
@ -184,9 +191,10 @@ export class EventActionsComponent {
if (this.event) {
this.pubkey = this.event.pubkey;
// this.profile = await this.profileService.getProfile(this.pubkey);
this.profile = await this.profileService.getProfile(this.pubkey);
} else if (this.profile) {
this.pubkey = this.profile.pubkey;
this.profile = await this.profileService.getProfile(this.pubkey);
} else {
// this.profile = await this.profileService.getProfile(this.pubkey);
}

View File

@ -1,10 +1,11 @@
<div class="bubble" [class.me]="message.id==0">
<div class="bubble" [class.me]="message.pubkey == me">
<div class="bubble cover">
<img class="bubble picture" [src]="message.cover" alt="">
<!-- <img class="bubble picture" [src]="message.cover" alt="" /> -->
<app-profile-image class="bubble picture" [publicKey]="message.pubkey"></app-profile-image>
</div>
<div class="bubble container">
<mat-card class="bubble card">
{{message.message}}
{{ message.content }}
</mat-card>
</div>
</div>

View File

@ -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();
}
}

View File

@ -0,0 +1,35 @@
<mat-nav-list>
<div class="search">
<mat-form-field appearance="fill" class="input-full-width">
<input matInput type="text" placeholder="Search...">
<mat-icon matSuffix>search</mat-icon>
</mat-form-field>
</div>
<ng-container *ngFor="let chat of chatService.chats$ | async; let lastItem = last">
<app-chat-item [routerLink]="['/m', chat.id]" [chat]="chat"></app-chat-item>
<mat-divider *ngIf="!lastItem"></mat-divider>
</ng-container>
<!-- <ng-container *ngFor="let event of chatService.chats2$ | async; let lastItem = last">
<app-chat-item [routerLink]="['/m', '5f432a9f39b58ff132fc0a4c8af10d42efd917d8076f68bb7f2f91ed7d4f6a41']" [event]="event"></app-chat-item>
<mat-divider *ngIf="!lastItem"></mat-divider>
</ng-container> -->
</mat-nav-list>
<div *ngFor="let chat of chatService.chats3">{{ chat.pubkey }} : {{ chat.content }}</div>
<!-- <div *ngIf="chatService.uniqueChats$ | async as chats">
<div *ngFor="let chat of chats">{{ chat.pubkey }} : {{ chat.content }}</div>
</div> -->
<!-- <div *ngIf="chats$ | async as chats">
<div *ngFor="let chat of chats">{{ chat.id }} : {{ chat.name }}</div>
</div> -->
<button (click)="add()">Add</button>
<button (click)="reset()">Reset</button>
<button (click)="chatService.download()">Download</button>

View File

@ -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;
}

View File

@ -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<ChatListComponent>;
// beforeEach(async(() => {
// TestBed.configureTestingModule({
// declarations: [ChatListComponent]
// })
// .compileComponents();
// }));
// beforeEach(() => {
// fixture = TestBed.createComponent(ChatListComponent);
// component = fixture.componentInstance;
// fixture.detectChanges();
// });
// it('should create', () => {
// expect(component).toBeTruthy();
// });
// });

View File

@ -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<string> = 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 = [];
}
}

View File

@ -2,8 +2,9 @@
<!-- <div [style.background-image]="utilities.getBannerBackgroundStyle(profile.banner)" class="profile-banner"></div> -->
<div class="profile-banner">
<img class="profile-banner-image" [src]="profile.banner" />
</div>
<img class="profile-banner-image" [src]="profile.banner || 'https://i.pinimg.com/564x/0b/a3/d6/0ba3d60362c7e6d256cfc1f37156bad9.jpg'" />
</div>
<div class="profile-page-header">
<div class="profile-page-header-left">