Lots of general UX improvements

This commit is contained in:
SondreB 2022-12-26 23:24:49 +01:00
parent 15de678496
commit 1ab0f41061
No known key found for this signature in database
GPG Key ID: D6CC44C75005FDBF
13 changed files with 299 additions and 26 deletions

View File

@ -11,6 +11,7 @@ import { SettingsComponent } from './settings/settings.component';
import { UserComponent } from './user/user.component'; import { UserComponent } from './user/user.component';
import { CirclesComponent } from './circles/circles.component'; import { CirclesComponent } from './circles/circles.component';
import { PeopleComponent } from './people/people.component'; import { PeopleComponent } from './people/people.component';
import { NoteComponent } from './note/note.component';
const routes: Routes = [ const routes: Routes = [
{ {
@ -47,6 +48,11 @@ const routes: Routes = [
component: UserComponent, component: UserComponent,
canActivate: [AuthGuard], canActivate: [AuthGuard],
}, },
{
path: 'note/:id',
component: NoteComponent,
canActivate: [AuthGuard],
},
{ {
path: 'about', path: 'about',
component: AboutComponent, component: AboutComponent,

View File

@ -61,6 +61,7 @@ import { FeedPrivateComponent } from './feed-private/feed-private.component';
import { FeedPublicComponent } from './feed-public/feed-public.component'; import { FeedPublicComponent } from './feed-public/feed-public.component';
import { NotesComponent } from './notes/notes.component'; import { NotesComponent } from './notes/notes.component';
import { NgxColorsModule } from 'ngx-colors'; import { NgxColorsModule } from 'ngx-colors';
import { NoteComponent } from './note/note.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -87,6 +88,7 @@ import { NgxColorsModule } from 'ngx-colors';
FeedPrivateComponent, FeedPrivateComponent,
FeedPublicComponent, FeedPublicComponent,
NotesComponent, NotesComponent,
NoteComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View File

@ -1,4 +1,5 @@
<span *ngIf="feedService.threadedEvents$ | async as events"> <span *ngIf="feedService.filteredEvents$ | async as events">
<mat-progress-bar *ngIf="events == null" mode="indeterminate"></mat-progress-bar> <mat-progress-bar *ngIf="events == null" mode="indeterminate"></mat-progress-bar>
<div class="page loading-container" *ngIf="!profileService.initialized"> <div class="page loading-container" *ngIf="!profileService.initialized">
@ -37,24 +38,6 @@
</mat-grid-list> </mat-grid-list>
<button class="follow-button" mat-raised-button color="primary" (click)="follow()">Follow</button> <button class="follow-button" mat-raised-button color="primary" (click)="follow()">Follow</button>
<!-- <div *ngFor="let profile of defaults">
<mat-card class="default-card">
<mat-card-header>
<img mat-card-avatar [src]="profile.picture" />
<mat-card-title>{{ profile.name }}</mat-card-title>
<mat-card-subtitle>{{ profile.about }}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<p class="wrap">
{{ profile.pubkey }}
</p>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="primary">Follow</button>
</mat-card-actions>
</mat-card>
</div> -->
</div> </div>
<div class="feed-page" *ngIf="events.length > 0"> <div class="feed-page" *ngIf="events.length > 0">

View File

@ -95,8 +95,9 @@ export class FeedPrivateComponent {
// this.events = this.validator.filterEvents(this.events); // this.events = this.validator.filterEvents(this.events);
} }
// TODO: FIX THIS IMMEDIATELY FOR PERFORMANCE!
hashtags(tags: any[]) { hashtags(tags: any[]) {
const hashtags = tags.map((row) => { const hashtags = tags.filter((row) => {
if (row[0] === 't') { if (row[0] === 't') {
return row[1]; return row[1];
} }

View File

@ -0,0 +1,4 @@
.original-post {
margin-bottom: 2em;
background-color: #303030;
}

View File

@ -0,0 +1,47 @@
<!-- <mat-progress-bar *ngIf="!events || events.length == 0" mode="indeterminate"></mat-progress-bar> -->
<div class="feed-page">
<div class="original-post events noclick" *ngIf="event">
<app-profile-actions [event]="event" [pubkey]="event.pubkey"></app-profile-actions>
<app-profile-header [pubkey]="event.pubkey"
><span class="event-date">{{ event.created_at | ago }}</span> <app-directory-icon [pubkey]="event.pubkey"></app-directory-icon
></app-profile-header>
<div class="content">
{{ event.content }}<span *ngIf="event.contentCut">... (message was truncated)</span>
<div class="dimmed">{{ rootLikes() }} likes&nbsp;&nbsp;{{ rootReplies() }} replies</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="page" *ngIf="feedService.thread.length === 0">
<h3 class="marginless">Loading thread... this can take some seconds.</h3>
</div>
<div class="feed-page" *ngIf="feedService.thread.length > 0">
<!-- <mat-accordion class="options">
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Options</mat-panel-title>
<mat-panel-description>{{ activeOptions() }}</mat-panel-description>
</mat-expansion-panel-header>
<p>
<mat-slide-toggle class="options-slider" [(ngModel)]="settings.options.paused">Pause feed</mat-slide-toggle>
<mat-slide-toggle class="options-slider" [(ngModel)]="settings.options.hideSpam" (ngModelChange)="optionsUpdated()">Hide spam</mat-slide-toggle>
<mat-slide-toggle class="options-slider" [(ngModel)]="settings.options.hideInvoice" (ngModelChange)="optionsUpdated()">Hide LN invoices</mat-slide-toggle>
</p>
</mat-expansion-panel>
</mat-accordion> -->
<div class="events" *ngFor="let event of filteredThread(); trackBy: trackByFn">
<span *ngIf="event.kind != 7">
<app-profile-actions [event]="event" [pubkey]="event.pubkey"></app-profile-actions>
<app-profile-header [pubkey]="event.pubkey"
><span class="event-date">{{ event.created_at | ago }}</span> <app-directory-icon [pubkey]="event.pubkey"></app-directory-icon
></app-profile-header>
<div class="content">{{ event.content }}<span *ngIf="event.contentCut">... (message was truncated)</span></div>
</span>
</div>
</div>

View File

@ -0,0 +1,165 @@
import { ChangeDetectorRef, Component } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ApplicationState } from '../services/applicationstate.service';
import { Utilities } from '../services/utilities.service';
import { relayInit, Relay } from 'nostr-tools';
import * as moment from 'moment';
import { DataValidation } from '../services/data-validation.service';
import { NostrEvent, NostrEventDocument, NostrProfile, NostrProfileDocument } from '../services/interfaces';
import { ProfileService } from '../services/profile.service';
import { SettingsService } from '../services/settings.service';
import { FeedService } from '../services/feed.service';
import { map, Observable } from 'rxjs';
@Component({
selector: 'app-note',
templateUrl: './note.component.html',
styleUrls: ['./note.component.css'],
})
export class NoteComponent {
id?: string | null;
event?: NostrEventDocument;
// userEvents$ = this.feedService.replyEvents$.pipe(
// map((data) => {
// return data.filter((events) => {
// events.tags
// const eTag = events.tags.find((t) => t[0] === 'e');
// if (eTag[1] == this.id) {
// return ;
// }
// console.log(eTag[1]);
// });
// // data.filter((events) => events.tags.find((t) => t[0] === 'e')
// // return data.filter((events) => events.tags.find((t) => t[0] === 'e'));
// // return data;
// })
// );
// replyEvents$ = this.feedService.events$.pipe(
// map((data) => {
// if (!this.pubkey) {
// return;
// }
// return data.filter((n) => n.pubkey == this.pubkey);
// })
// );
constructor(
public appState: ApplicationState,
private activatedRoute: ActivatedRoute,
private cd: ChangeDetectorRef,
public settings: SettingsService,
public feedService: FeedService,
public profiles: ProfileService,
private validator: DataValidation,
private utilities: Utilities,
private router: Router
) {
// this.appState.title = 'Blockcore Notes';
this.appState.showBackButton = true;
this.activatedRoute.paramMap.subscribe(async (params) => {
const id: any = params.get('id');
if (!id) {
this.router.navigateByUrl('/');
return;
}
this.id = id;
this.event = this.feedService.events.find((e) => e.id == this.id);
if (!this.event) {
// this.router.navigateByUrl('/');
return;
}
this.appState.title = `Note: ${this.event.content.substring(0, 200)}`;
// Clear the initial thread:
this.feedService.thread = [];
// First download all posts, if any, that is mentioned in the e tags.
this.feedService.downloadThread(this.id!);
});
}
rootLikes() {
const eventsWithSingleeTag = this.feedService.thread.filter((e) => e.kind == 7 && e.tags.filter((p) => p[0] === 'e').length == 1);
return eventsWithSingleeTag.length;
}
rootReplies() {
const eventsWithSingleeTag = this.feedService.thread.filter((e) => e.kind != 7 && e.tags.filter((p) => p[0] === 'e').length == 1);
return eventsWithSingleeTag.length;
}
filteredThread() {
return this.feedService.thread.filter((p) => p.kind != 7);
}
ngOnInit() {
// if (this.pubkey) {
// console.log('PIPING EVENTS...');
// this.userEvents$ =
// }
}
openEvent($event: any, event: NostrEventDocument) {
const paths = $event.composedPath();
if (!paths || paths.length == 0) {
return;
}
if (paths[0].className.indexOf('clickable') == -1) {
return;
}
this.router.navigate(['/note', event.id]);
}
optionsUpdated() {
// this.allComplete = this.task.subtasks != null && this.task.subtasks.every(t => t.completed);
// Parse existing content.
// this.events = this.validator.filterEvents(this.events);
}
activeOptions() {
let options = '';
if (this.settings.options.hideSpam) {
options += ' Spam: Filtered';
} else {
options += ' Spam: Allowed';
}
if (this.settings.options.hideInvoice) {
options += ' Invoices: Hidden';
} else {
options += ' Invoices: Displayed';
}
return options;
}
public trackByFn(index: number, item: NostrEvent) {
return item.id;
}
sub: any;
initialLoad = true;
ngOnDestroy() {
if (this.sub) {
this.sub.unsub();
}
}
}

View File

@ -38,12 +38,12 @@
</mat-list-item> </mat-list-item>
</mat-list> --> </mat-list> -->
<div class="events" *ngFor="let profile of profiles; trackBy: trackByFn"> <div class="events clickable" (click)="openProfile($event, profile)" *ngFor="let profile of profiles; trackBy: trackByFn">
<app-profile-actions [profile]="profile"></app-profile-actions> <app-profile-actions [profile]="profile"></app-profile-actions>
<app-profile-header [profile]="profile"><span *ngIf="profile.created">Started following {{ profile.created | ago }}</span> <app-directory-icon [pubkey]="profile.pubkey"></app-directory-icon></app-profile-header> <app-profile-header [profile]="profile"><span *ngIf="profile.created">Started following {{ profile.created | ago }}</span> <app-directory-icon [pubkey]="profile.pubkey"></app-directory-icon></app-profile-header>
<div class="content"> <span class="content">
{{ profile.about }} {{ profile.about }}
</div> </span>
</div> </div>
</div> </div>

View File

@ -102,6 +102,20 @@ export class PeopleComponent {
}); });
} }
openProfile($event: any, event: NostrProfileDocument) {
const paths = $event.composedPath();
if (!paths || paths.length == 0) {
return;
}
if (paths[0].className.indexOf('clickable') == -1) {
return;
}
this.router.navigate(['/user', event.pubkey]);
}
async search() { async search() {
const text: string = this.searchTerm; const text: string = this.searchTerm;

View File

@ -276,6 +276,47 @@ export class FeedService {
}); });
} }
// TODO: Temporary container for thread events. The downloadThread should probably return an Observable that should
// vanish when the user is finished watched it.
thread: NostrEvent[] = [];
// threadQueue: string[];
downloadThread(id: string) {
const relay = this.relays[0];
const backInTime = moment().subtract(120, 'minutes').unix();
const sub = relay.sub([{ ['#e']: [id] }], {}) as NostrSubscription;
sub.loading = true;
// Keep all subscriptions around so we can close them when needed.
this.subs.push(sub);
sub.on('event', (originalEvent: any) => {
const event = this.eventService.processEvent(originalEvent);
if (!event) {
return;
}
const eventIndex = this.thread.findIndex((e) => e.id == event.id);
if (eventIndex > -1) {
this.thread[eventIndex] = event;
} else {
this.thread.unshift(event);
}
});
sub.on('eose', () => {
console.log('Initial load of people feed completed.');
sub.loading = false;
sub.unsub();
});
}
async downloadProfile(pubkey: string) { async downloadProfile(pubkey: string) {
console.log('ADD DOWNLOAD PROFILE:', pubkey); console.log('ADD DOWNLOAD PROFILE:', pubkey);
if (!pubkey) { if (!pubkey) {

View File

@ -8,7 +8,7 @@
<!-- <app-profile-image [publicKey]="event.pubkey"></app-profile-image> --> <!-- <app-profile-image [publicKey]="event.pubkey"></app-profile-image> -->
</a> </a>
</div> </div>
<div class="name"> <div class="name clickable">
<a [routerLink]="['/user', pubkey]"> <a [routerLink]="['/user', pubkey]">
<span [class.muted]="muted" [matTooltip]="tooltipName" matTooltipPosition="above">{{ profileName }}</span> <span [class.muted]="muted" [matTooltip]="tooltipName" matTooltipPosition="above">{{ profileName }}</span>
<!-- <span *ngIf="!tooltip" [matTooltip]="tooltipName">{{ profileName }}</span> --> <!-- <span *ngIf="!tooltip" [matTooltip]="tooltipName">{{ profileName }}</span> -->

View File

@ -33,7 +33,7 @@
><span class="event-date">{{ event.created_at | ago }}</span> <app-directory-icon [pubkey]="event.pubkey"></app-directory-icon ><span class="event-date">{{ event.created_at | ago }}</span> <app-directory-icon [pubkey]="event.pubkey"></app-directory-icon
></app-profile-header> ></app-profile-header>
<div class="content">{{ event.content }}<span *ngIf="event.contentCut">... (message was truncated)</span></div> <span class="content">{{ event.content }}<span *ngIf="event.contentCut">... (message was truncated)</span></span>
</div> </div>
</div> </div>
</span> </span>

View File

@ -198,11 +198,21 @@ mat-sidenav-content mat-toolbar {
border-radius: 10px; border-radius: 10px;
} }
.events:hover { // .events {
// background-color: #535353;
// cursor: pointer;
// }
.events.clickable:hover {
background-color: #535353; background-color: #535353;
cursor: pointer; cursor: pointer;
} }
.noclick:hover {
background-color: #424242;
cursor: default;
}
.items { .items {
margin-top: 1em; margin-top: 1em;
padding: 1em; padding: 1em;