Merge pull request #124 from block-core/new-state-management

WIP: New state management
This commit is contained in:
Lu 2023-05-12 13:45:28 +03:00 committed by GitHub
commit 8350889b8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 307 additions and 22 deletions

View File

@ -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 { ExampleComponent } from './example/example';
const routes: Routes = [
{
@ -245,7 +246,7 @@ const routes: Routes = [
},
{
path: 'about',
loadChildren: () => import('./about/about.module').then(m => m.AboutModule)
loadChildren: () => import('./about/about.module').then((m) => m.AboutModule),
// component: AboutComponent,
// canActivate: [AuthGuard],
// resolve: {
@ -284,6 +285,14 @@ const routes: Routes = [
data: LoadingResolverService,
},
},
{
path: 'example',
component: ExampleComponent,
canActivate: [AuthGuard],
resolve: {
data: LoadingResolverService,
},
},
{
path: 'queue',
component: QueueComponent,

View File

@ -110,6 +110,10 @@
<!-- <a [routerLink]="['/development']" mat-menu-item (click)="toggleMenu()" [routerLinkActiveOptions]="{ exact: true }" routerLinkActive="active">
<mat-icon>construction</mat-icon>
<span *ngIf="displayLabels">Development</span>
</a>
<a [routerLink]="['/example']" mat-menu-item (click)="toggleMenu()" [routerLinkActiveOptions]="{ exact: true }" routerLinkActive="active">
<mat-icon>construction</mat-icon>
<span *ngIf="displayLabels">Example</span>
</a> -->
<a [routerLink]="['/relays']" mat-menu-item (click)="toggleMenu()" [routerLinkActiveOptions]="{ exact: true }" routerLinkActive="active">
<mat-icon>dns</mat-icon>

View File

@ -153,6 +153,7 @@ import { BadgeComponent } from './badge/badge';
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';
@NgModule({
declarations: [
AppComponent,
@ -244,6 +245,7 @@ import { ZappersListDialogComponent } from './shared/zappers-list-dialog/zappers
TagsComponent,
BadgeComponent,
ZappersListDialogComponent,
ExampleComponent
],
imports: [
HttpClientModule,

View File

@ -27,6 +27,8 @@ import { OptionsService } from './services/options';
import { LabelService } from './services/label';
import { TranslateService } from '@ngx-translate/core';
import { BadgeService } from './services/badge';
import { State } from './services/state';
import { EventService } from './services/event';
@Component({
selector: 'app-root',
@ -66,7 +68,9 @@ export class AppComponent {
public ui: UIService,
private bottomSheet: MatBottomSheet,
public searchService: SearchService,
public theme: ThemeService
public theme: ThemeService,
private state: State,
private eventService: EventService
) {
if (!this.visibilityHandler) {
this.visibilityHandler = addEventListener('visibilitychange', (event) => {
@ -96,7 +100,8 @@ export class AppComponent {
}
this.authService.authInfo$.subscribe(async (auth) => {
auth.publicKeyHex;
this.state.pubkey = auth.publicKeyHex;
this.authenticated = auth.authenticated();

View File

@ -1,4 +1,93 @@
<div class="page">
<h1>State Service Management</h1>
<mat-list >
<div mat-subheader>Metadata</div>
<mat-list-item *ngFor="let state of state.events.metadata | keyvalue">
<mat-icon matListItemIcon>folder</mat-icon>
<div matListItemTitle>{{state.key}}</div>
<div matListItemLine>{{state.value.content}}</div>
</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list >
<div mat-subheader>Text</div>
<mat-list-item *ngFor="let state of state.events.shortTextNote | keyvalue">
<mat-icon matListItemIcon>folder</mat-icon>
<div matListItemTitle>{{state.key}}</div>
<div matListItemLine>{{state.value.content}}</div>
</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list >
<div mat-subheader>Contacts</div>
<mat-list-item *ngFor="let state of state.events.contacts | keyvalue">
<mat-icon matListItemIcon>folder</mat-icon>
<div matListItemTitle>{{state.key}}</div>
<div matListItemLine>{{state.value.content}}</div>
</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list >
<div mat-subheader>Reactions</div>
<mat-list-item *ngFor="let state of state.events.reaction | keyvalue">
<mat-icon matListItemIcon>folder</mat-icon>
<div matListItemTitle>{{state.key}}</div>
<div matListItemLine>{{state.value.content}}</div>
</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list >
<div mat-subheader>Reposts</div>
<mat-list-item *ngFor="let state of state.events.reposts| keyvalue">
<mat-icon matListItemIcon>folder</mat-icon>
<div matListItemTitle>{{state.key}}</div>
<div matListItemLine>{{state.value.content}}</div>
</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list >
<div mat-subheader>Zap</div>
<mat-list-item *ngFor="let state of state.events.zap | keyvalue">
<mat-icon matListItemIcon>folder</mat-icon>
<div matListItemTitle>{{state.key}}</div>
<div matListItemLine>{{state.value.content}}</div>
</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list >
<div mat-subheader>Zap Request</div>
<mat-list-item *ngFor="let state of state.events.zapRequest| keyvalue">
<mat-icon matListItemIcon>folder</mat-icon>
<div matListItemTitle>{{state.key}}</div>
<div matListItemLine>{{state.value.content}}</div>
</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list >
<div mat-subheader>Article</div>
<mat-list-item *ngFor="let state of state.events.longFormContent | keyvalue">
<mat-icon matListItemIcon>folder</mat-icon>
<div matListItemTitle>{{state.key}}</div>
<div matListItemLine>{{state.value.content}}</div>
</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
State as JSON:<br>
{{ state | json }}
<br /><br />
<p>This page act as examples for more specialized implementation details of the app.</p>
<button mat-stroked-button (click)="downloadProfile()">Enque Profile Download</button>
@ -21,4 +110,4 @@
<app-relays [relays]="relayService.items"></app-relays>
<!-- <mat-spinner></mat-spinner> -->
</div>
</div>

View File

@ -5,6 +5,7 @@ import { NostrService } from '../services/nostr';
import { RelayService } from '../services/relay';
import { RelayType } from '../types/relay';
import { Storage } from '../types/storage';
import { State, StateService } from '../services/state';
@Component({
selector: 'app-development',
@ -15,10 +16,13 @@ export class DevelopmentComponent {
worker?: Worker;
storage?: Storage;
constructor(private nostr: NostrService, private dataService: DataService, private appState: ApplicationState, public relayService: RelayService) {}
constructor(
public state: State,
private nostr: NostrService, private dataService: DataService, private appState: ApplicationState, public relayService: RelayService) {}
ngOnInit() {
this.appState.updateTitle('Development & Debug');
}
async database() {

View File

@ -0,0 +1,19 @@
.example-viewport {
height: 100%;
min-height: 500px;
width: 100%;
border: 1px solid black;
}
.example-item {
box-sizing: border-box;
height: 200px;
background-color: purple;
}
.example-item-content {
overflow: scroll;
width: 100%;
height: 100px;
border: 1px solid green;
}

View File

@ -0,0 +1,12 @@
<div class="feed-page">
<p>This example demonstrates how to do sorting, filtering, virtual scrolling, dynamic updates of complex data within Blockcore Notes.</p>
<cdk-virtual-scroll-viewport itemSize="200" class="example-viewport" #scrollViewport>
<div *cdkVirtualFor="let item of state.events.shortTextNote | keyvalue" class="example-item">
{{item.value.id}}
<div class="example-item-content">{{item.value.content}}</div>
</div>
</cdk-virtual-scroll-viewport>
</div>

View File

@ -0,0 +1,52 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { ApplicationState } from '../services/applicationstate';
import { State } from '../services/state';
@Component({
selector: 'app-example',
templateUrl: 'example.html',
styleUrls: ['example.css'],
// changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ExampleComponent implements OnInit {
@ViewChild('scrollViewport')
private cdkVirtualScrollViewport: any;
items = Array.from({ length: 10000 }).map((_, i) => `Item #${i + 1}`);
constructor(public state: State, private appState: ApplicationState) {}
ngOnInit() {
this.appState.updateTitle('Example');
}
calculateContainerHeight(): string {
const numberOfItems = this.items.length;
// This should be the height of your item in pixels
const itemHeight = 20;
// The final number of items you want to keep visible
const visibleItems = 10;
setTimeout(() => {
// Makes CdkVirtualScrollViewport to refresh its internal size values after
// changing the container height. This should be delayed with a "setTimeout"
// because we want it to be executed after the container has effectively
// changed its height. Another option would be a resize listener for the
// container and call this line there but it may requires a library to detect
// the resize event.
this.cdkVirtualScrollViewport.checkViewportSize();
}, 300);
// It calculates the container height for the first items in the list
// It means the container will expand until it reaches `200px` (20 * 10)
// and will keep this size.
if (numberOfItems <= visibleItems) {
return `${itemHeight * numberOfItems}px`;
}
// This function is called from the template so it ensures the container will have
// the final height if number of items are greater than the value in "visibleItems".
return `${itemHeight * visibleItems}px`;
}
}

View File

@ -1,4 +1,3 @@
.loading-container {
text-align: center;
}
@ -7,3 +6,6 @@
margin: auto;
}
.circle-selection {
margin-bottom: 1em;
}

View File

@ -12,6 +12,13 @@
</div> -->
<div class="feed-page">
<mat-button-toggle-group class="circle-selection" name="ingredients">
<mat-button-toggle [routerLink]="['/feed', circle.id]" [matTooltip]="circle.name" value="flour" *ngFor="let circle of circleService.circles">
<mat-icon matListItemIcon [style.color]="circle.color">trip_origin</mat-icon>
</mat-button-toggle>
</mat-button-toggle-group>
<mat-accordion class="options">
<mat-expansion-panel>
<mat-expansion-panel-header>

View File

@ -13,6 +13,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { StorageService } from '../services/storage';
import { dexieToRx } from '../shared/utilities';
import { UIService } from '../services/ui';
import { CircleService } from '../services/circle';
@Component({
selector: 'app-feed-private',
@ -42,6 +43,7 @@ export class FeedPrivateComponent {
// }
constructor(
public circleService: CircleService,
public ui: UIService,
private activatedRoute: ActivatedRoute,
public db: StorageService,

View File

@ -7,7 +7,8 @@
<!-- <div *ngIf="profileService.item$ | async as profile">
{{ profile.name }}
</div> -->
<div class="page">
<ng-container *ngIf="ui.profile$ | async as profile">
<!-- <div class="tab-panel-viewport" cdkVirtualScrollingElement>
<cdk-virtual-scroll-viewport scrollWindow itemSize="1">
@ -23,6 +24,7 @@
</div>
</div>
</ng-container>
</div>
<!-- <div class="page" *ngIf="ui.profile$ | async as profile">
<app-profile-widget *ngFor="let pubkey of virtualList.view" [pubkey]="pubkey"></app-profile-widget>

View File

@ -100,7 +100,7 @@ export class PeopleComponent {
});
} else if (sorting === 'followed-desc') {
this.sortedItems = this.items.sort((a, b) => {
return a.followed! < b.followed! ? -1 : 1;
return a.followed! > b.followed! ? 1 : -1;
});
} else if (sorting === 'created-asc') {
this.sortedItems = this.items.sort((a, b) => {

View File

@ -9,15 +9,58 @@ export class StateService {
constructor(private state: State) {}
addEvent(event: NostrEvent) {
return;
// TODO: Temporarily removed to avoid building massive in-memory state.
// switch (event.kind) {
// case Kind.Metadata:
// this.addIfNewer(event, this.state.events.shortTextNote);
// break;
// case Kind.Text:
// this.addIfMissing(event, this.state.events.shortTextNote);
// break;
// }
switch (event.kind as any) {
case Kind.Metadata:
this.addIfNewer(event, event.pubkey, this.state.events.metadata);
if (this.state.pubkey == event.pubkey) {
this.state.metadata = event;
}
break;
case Kind.Text:
this.addIfMissing(event, this.state.events.shortTextNote);
break;
case Kind.Contacts:
this.addIfNewer(event, event.pubkey, this.state.events.contacts);
break;
case Kind.Reaction:
this.addIfMissing(event, this.state.events.reaction);
break;
case 6:
this.addIfMissing(event, this.state.events.reposts);
break;
case Kind.Zap:
this.addIfMissing(event, this.state.events.zap);
break;
case Kind.ZapRequest:
this.addIfMissing(event, this.state.events.zapRequest);
break;
case Kind.Article:
const slug = this.firstDTag(event);
this.addIfNewer(event, slug!, this.state.events.longFormContent);
break;
}
}
tagsOfType(event: NostrEvent | null, type: string) {
if (!event) {
return [];
}
const tags = event.tags.filter((t) => t[0] === type);
return tags;
}
firstDTag(event: NostrEvent | null | any) {
const tags = this.tagsOfType(event, 'd');
if (tags.length == 0) {
return undefined;
}
return tags[0][1];
}
addIfMissing(event: NostrEvent, map: Map<string, NostrEvent>) {
@ -28,17 +71,17 @@ export class StateService {
map.set(event.id, event);
}
addIfNewer(event: NostrEvent, map: Map<string, NostrEvent>) {
if (!map.has(event.id)) {
map.set(event.id, event);
addIfNewer(event: NostrEvent, identifier: string, map: Map<string, NostrEvent>) {
if (!map.has(identifier)) {
map.set(identifier, event);
} else {
const existing = map.get(event.id);
const existing = map.get(identifier);
if (existing!.created_at > event.created_at) {
if (existing!.created_at >= event.created_at) {
return;
}
map.set(event.id, event);
map.set(identifier, event);
}
}
}
@ -51,6 +94,10 @@ export class State {
circles: Circle[] = [];
metadata?: NostrEvent;
pubkey?: String;
events: EventsState = {
metadata: new Map(),
shortTextNote: new Map(),

View File

@ -15,6 +15,12 @@
<ng-container *ngFor="let token of dynamicText">
<ng-template [ngIf]="isString(token)">{{ token }}</ng-template>
<ng-template [ngIf]="!isString(token) && isFollowing" [ngSwitch]="token.token">
<ng-container *ngSwitchCase="'npub'"><a class="reply-link" [routerLink]="['/p', token.word]">@{{ getDisplayName(token.word) }}</a></ng-container>
<ng-container *ngSwitchCase="'nprofile'"><a class="reply-link" [routerLink]="['/p', token.word]">@{{ getDisplayName(token.word) }}</a></ng-container>
<ng-container *ngSwitchCase="'note'"><a class="reply-link" [routerLink]="['/e', token.word]">{{ token.word }}</a></ng-container>
<ng-container *ngSwitchCase="'nevent'"><a class="reply-link" [routerLink]="['/e', token.word]">{{ token.word }}</a></ng-container>
<ng-container *ngSwitchCase="'username'"><a class="reply-link" [routerLink]="['/p', token.word]">@{{ getDisplayName(token.word) }}</a></ng-container>
<ng-container *ngSwitchCase="'link'"><a [href]="[token.word]" target="_blank">{{ token.word }}</a></ng-container>
<ng-container *ngSwitchCase="'image'"><img mat-card-image class="event-image" loading="lazy" decoding="async" [matTooltip]="token.word" [alt]="token.word" [src]="token.safeWord" (click)="expandImage(token.word)" /></ng-container>
@ -47,6 +53,12 @@
</ng-template>
<ng-template [ngIf]="!isString(token) && !isFollowing" [ngSwitch]="token.token">
<ng-container *ngSwitchCase="'npub'"><a class="reply-link" [routerLink]="['/p', token.word]">@{{ getDisplayName(token.word) }}</a></ng-container>
<ng-container *ngSwitchCase="'nprofile'"><a class="reply-link" [routerLink]="['/p', token.word]">@{{ getDisplayName(token.word) }}</a></ng-container>
<ng-container *ngSwitchCase="'note'"><a class="reply-link" [routerLink]="['/e', token.word]">{{ token.word }}</a></ng-container>
<ng-container *ngSwitchCase="'nevent'"><a class="reply-link" [routerLink]="['/e', token.word]">{{ token.word }}</a></ng-container>
<ng-container *ngSwitchCase="'username'"><a class="reply-link" [routerLink]="['/p', token.word]">@{{ getDisplayName(token.word) }}</a></ng-container>
<ng-container *ngSwitchCase="'link'"><a [href]="[token.word]" target="_blank">{{ token.word }}</a></ng-container>
<ng-container *ngSwitchCase="'image'"><a [href]="[token.word]" target="_blank">{{ token.word }}</a></ng-container>

View File

@ -9,6 +9,7 @@ import { ProfileService } from 'src/app/services/profile';
import { Utilities } from 'src/app/services/utilities';
import { NostrEventDocument, NostrProfile, NostrProfileDocument, TokenKeyword } from '../../services/interfaces';
import { ProfileImageDialog } from '../profile-image-dialog/profile-image-dialog';
import { nip19 } from 'nostr-tools';
interface MediaItem {
url: SafeResourceUrl;
@ -132,6 +133,7 @@ export class ContentComponent {
}
getDisplayName(pubkey: string) {
pubkey = this.utilities.ensureHexIdentifier(pubkey);
const profile = this.profileService.getCachedProfile(pubkey);
if (!profile) {
@ -281,6 +283,21 @@ export class ContentComponent {
}
i = res.push(keyword);
} else if (token.startsWith('nostr:')) {
const decoded = nip19.decode(token.substring(6));
const val = decoded.data as any;
if (decoded.type === 'nprofile') {
i = res.push({ safeWord: this.utilities.sanitizeUrlAndBypass(token), word: val.pubkey, token: decoded.type });
} else if (decoded.type === 'npub') {
i = res.push({ safeWord: this.utilities.sanitizeUrlAndBypass(token), word: val, token: decoded.type });
} else if (decoded.type === 'note') {
i = res.push({ safeWord: this.utilities.sanitizeUrlAndBypass(token), word: val, token: decoded.type });
} else if (decoded.type === 'nevent') {
i = res.push({ safeWord: this.utilities.sanitizeUrlAndBypass(token), word: val.id, token: decoded.type });
} else {
i = res.push({ safeWord: this.utilities.sanitizeUrlAndBypass(token), word: token.substring(6), token: decoded.type });
}
} else if (token.startsWith('http://') || token.startsWith('https://')) {
if (this.isImage(token)) {
i = res.push({ safeWord: this.utilities.sanitizeUrlAndBypass(token), word: token, token: 'image' });