mirror of
https://github.com/block-core/blockcore-notes.git
synced 2024-09-29 06:20:42 +00:00
Add support for editing articles
This commit is contained in:
parent
0afa9e973f
commit
e37ee0188f
@ -2,12 +2,20 @@
|
||||
<h1>Write your thoughts</h1>
|
||||
|
||||
<div class="note-type">
|
||||
<mat-button-toggle-group name="fontStyle" [(ngModel)]="eventType" aria-label="Font Style" #group="matButtonToggleGroup">
|
||||
<mat-button-toggle-group name="fontStyle" (change)="noteTypeChanged()" [(ngModel)]="eventType" aria-label="Font Style" #group="matButtonToggleGroup">
|
||||
<mat-button-toggle value="text">Note</mat-button-toggle>
|
||||
<!-- <mat-button-toggle value="article">Article</mat-button-toggle> -->
|
||||
<mat-button-toggle value="article">Article</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</div>
|
||||
|
||||
<mat-form-field appearance="fill" *ngIf="group.value == 'article'">
|
||||
<mat-label>Existing articles</mat-label>
|
||||
<mat-select [(value)]="selectedArticle" (selectionChange)="changedArticle()">
|
||||
<mat-option></mat-option>
|
||||
<mat-option [value]="article.slug" *ngFor="let article of articleService.articles">{{ article.title }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<form [formGroup]="articleForm" *ngIf="group.value == 'article'" (ngSubmit)="onSubmitArticle()">
|
||||
<div mat-dialog-content class="mat-dialog-content">
|
||||
<mat-form-field appearance="outline" class="input-full-width">
|
||||
@ -40,6 +48,8 @@
|
||||
<input matInput #message placeholder="Tech, News, Social" formControlName="tags" />
|
||||
</mat-form-field>
|
||||
|
||||
<input type="hidden" formControlName="published_at" />
|
||||
|
||||
<emoji-mart class="picker" *ngIf="isEmojiPickerVisible" emoji="point_up" [isNative]="true" [showPreview]="false" (emojiSelect)="addEmojiArticle($event)" title="Choose your emoji"></emoji-mart>
|
||||
<mat-icon class="toolbar-icon margin-right" (click)="isEmojiPickerVisible = !isEmojiPickerVisible;" matTooltip="Insert emoji">sentiment_satisfied</mat-icon>
|
||||
<!-- <mat-icon class="toolbar-icon margin-right" matTooltip="Upload file">attach_file</mat-icon> -->
|
||||
@ -55,7 +65,8 @@
|
||||
</mat-form-field> -->
|
||||
</div>
|
||||
<div mat-dialog-actions class="mat-dialog-actions" align="end">
|
||||
<button mat-stroked-button type="button" (click)="onCancel()">Cancel</button> <button type="button" mat-stroked-button disabled="disabled">Save Draft</button>
|
||||
<button mat-stroked-button type="button" (click)="onCancel()">Cancel</button>
|
||||
<!-- <button type="button" mat-stroked-button disabled="disabled">Save Draft</button> -->
|
||||
<button mat-flat-button [disabled]="!articleForm.valid" type="submit" color="primary">Publish Article</button>
|
||||
</div>
|
||||
</form>
|
||||
@ -82,7 +93,8 @@
|
||||
</mat-form-field> -->
|
||||
</div>
|
||||
<div mat-dialog-actions class="mat-dialog-actions" align="end">
|
||||
<button mat-stroked-button type="button" (click)="onCancel()">Cancel</button> <button mat-stroked-button type="button" disabled="disabled">Save Draft</button>
|
||||
<button mat-stroked-button type="button" (click)="onCancel()">Cancel</button>
|
||||
<!-- <button mat-stroked-button type="button" disabled="disabled">Save Draft</button> -->
|
||||
<button mat-flat-button [disabled]="!noteForm.valid" type="submit" color="primary">Publish Note</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -7,6 +7,9 @@ import { BlogEvent } from '../services/interfaces';
|
||||
import { Event } from 'nostr-tools';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Utilities } from '../services/utilities';
|
||||
import { QueueService } from '../services/queue.service';
|
||||
import { ArticleService } from '../services/article';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
|
||||
export interface NoteDialogData {
|
||||
note: string;
|
||||
@ -37,6 +40,7 @@ export class EditorComponent {
|
||||
image: [''],
|
||||
slug: [''],
|
||||
tags: [''],
|
||||
published_at: [''],
|
||||
});
|
||||
|
||||
note: string = '';
|
||||
@ -55,7 +59,16 @@ export class EditorComponent {
|
||||
|
||||
subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(private utilities: Utilities, private appState: ApplicationState, private location: Location, private fb: FormBuilder, public navigation: NavigationService) {}
|
||||
constructor(
|
||||
private snackBar: MatSnackBar,
|
||||
public articleService: ArticleService,
|
||||
private queueService: QueueService,
|
||||
private utilities: Utilities,
|
||||
private appState: ApplicationState,
|
||||
private location: Location,
|
||||
private fb: FormBuilder,
|
||||
public navigation: NavigationService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.appState.updateTitle(`Write a note`);
|
||||
@ -74,10 +87,51 @@ export class EditorComponent {
|
||||
);
|
||||
}
|
||||
|
||||
selectedArticle: string = '';
|
||||
|
||||
changedArticle() {
|
||||
const article = this.articleService.get(this.selectedArticle!);
|
||||
|
||||
if (!article) {
|
||||
this.articleForm.reset();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (article.summary == null) {
|
||||
article.summary = '';
|
||||
}
|
||||
|
||||
if (article.image == null) {
|
||||
article.image = '';
|
||||
}
|
||||
|
||||
if (article.title == null) {
|
||||
article.title = '';
|
||||
}
|
||||
|
||||
this.articleForm.setValue({
|
||||
content: article.content,
|
||||
title: article.title,
|
||||
summary: article.summary,
|
||||
image: article.image,
|
||||
slug: article.slug ? article.slug : '',
|
||||
tags: article.metatags ? article.metatags.toString() : '',
|
||||
published_at: article.published_at ? article.published_at.toString() : '',
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.utilities.unsubscribe(this.subscriptions);
|
||||
}
|
||||
|
||||
noteTypeChanged() {
|
||||
// Load all articles for the user when toggling.
|
||||
if (this.eventType == 'article') {
|
||||
this.queueService.enque(this.appState.getPublicKey(), 'Article');
|
||||
}
|
||||
}
|
||||
|
||||
createSlug(input: string) {
|
||||
// convert input to lowercase
|
||||
input = input.toLowerCase();
|
||||
@ -111,20 +165,11 @@ export class EditorComponent {
|
||||
(<any>this.articleContent).nativeElement.focus();
|
||||
}
|
||||
|
||||
postBlog() {
|
||||
// this.formGroupBlog.controls.
|
||||
|
||||
// this.profileForm.value
|
||||
|
||||
console.log('BLOG:', this.blog);
|
||||
// this.navigation.saveNote(this.note);
|
||||
}
|
||||
|
||||
formatSlug() {
|
||||
this.articleForm.controls.slug.setValue(this.createSlug(this.articleForm.controls.slug.value!));
|
||||
}
|
||||
|
||||
onSubmitArticle() {
|
||||
async onSubmitArticle() {
|
||||
const controls = this.articleForm.controls;
|
||||
|
||||
const blog: BlogEvent = {
|
||||
@ -136,7 +181,17 @@ export class EditorComponent {
|
||||
tags: controls.tags.value!,
|
||||
};
|
||||
|
||||
this.navigation.saveArticle(blog);
|
||||
if (controls.published_at.value) {
|
||||
blog.published_at = Number(controls.published_at.value);
|
||||
}
|
||||
|
||||
await this.navigation.saveArticle(blog);
|
||||
|
||||
this.snackBar.open(`Article was published. Notes does not support viewing articles yet.`, 'Hide', {
|
||||
duration: 2000,
|
||||
horizontalPosition: 'center',
|
||||
verticalPosition: 'bottom',
|
||||
});
|
||||
}
|
||||
|
||||
onSubmitNote() {
|
||||
|
55
src/app/services/article.ts
Normal file
55
src/app/services/article.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ApplicationState } from './applicationstate';
|
||||
import { EventService } from './event';
|
||||
import { NostrArticle, NostrEvent } from './interfaces';
|
||||
import { Utilities } from './utilities';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ArticleService {
|
||||
articles: NostrArticle[] = [];
|
||||
|
||||
constructor(private appState: ApplicationState, private utilities: Utilities, private eventService: EventService) {}
|
||||
|
||||
get(slug: string) {
|
||||
return this.articles.find((a) => a.slug == slug);
|
||||
}
|
||||
|
||||
put(event: NostrEvent) {
|
||||
const article = event as NostrArticle;
|
||||
article.slug = this.eventService.lastDTag(event);
|
||||
article.title = this.eventService.lastTagOfType(event, 'title');
|
||||
article.summary = this.eventService.lastTagOfType(event, 'summary');
|
||||
article.image = this.eventService.lastTagOfType(event, 'image');
|
||||
article.metatags = this.eventService.tagsOfTypeValues(event, 't');
|
||||
article.metatags = this.eventService.tagsOfTypeValues(event, 't');
|
||||
|
||||
const publishedAt = this.eventService.lastTagOfType(event, 'published_at');
|
||||
|
||||
if (publishedAt) {
|
||||
article.published_at = Number(publishedAt);
|
||||
}
|
||||
|
||||
if (article.pubkey == this.appState.getPublicKey()) {
|
||||
const index = this.articles.findIndex((a) => a.slug == article.slug);
|
||||
|
||||
if (index > -1) {
|
||||
const existing = this.articles[index];
|
||||
|
||||
// If the existing is newer, ignore this article.
|
||||
if (existing.created_at > article.created_at) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace when newer.
|
||||
this.articles[index] = article;
|
||||
} else {
|
||||
this.articles.push(article);
|
||||
}
|
||||
} else {
|
||||
// TODO: We currently don't support lookup of others articles, when the time comes, update this.
|
||||
debugger;
|
||||
}
|
||||
}
|
||||
}
|
@ -67,6 +67,54 @@ export class EventService {
|
||||
return tags[tags.length - 1][1];
|
||||
}
|
||||
|
||||
titleTag(event: NostrEventDocument | null) {
|
||||
const tags = this.tagsOfType(event, 'title');
|
||||
|
||||
if (tags.length == 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return tags[tags.length - 1][1];
|
||||
}
|
||||
|
||||
lastDTag(event: NostrEventDocument | null) {
|
||||
const tags = this.tagsOfType(event, 'd');
|
||||
|
||||
if (tags.length == 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return tags[tags.length - 1][1];
|
||||
}
|
||||
|
||||
lastTagOfType(event: NostrEventDocument | null, type: string) {
|
||||
const tags = this.tagsOfType(event, type);
|
||||
|
||||
if (tags.length == 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return tags[tags.length - 1][1];
|
||||
}
|
||||
|
||||
tagsOfType(event: NostrEventDocument | null, type: string) {
|
||||
if (!event) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const tags = event.tags.filter((t) => t[0] === type);
|
||||
return tags;
|
||||
}
|
||||
|
||||
tagsOfTypeValues(event: NostrEventDocument | null, type: string) {
|
||||
if (!event) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const tags = event.tags.filter((t) => t[0] === type);
|
||||
return tags.map((t) => t[1]);
|
||||
}
|
||||
|
||||
eTags(event: NostrEventDocument | null) {
|
||||
if (!event) {
|
||||
return [];
|
||||
|
@ -25,7 +25,7 @@ export interface Contact {
|
||||
}
|
||||
|
||||
export interface QueryJob {
|
||||
type: 'Profile' | 'Event' | 'Contacts';
|
||||
type: 'Profile' | 'Event' | 'Contacts' | 'Article';
|
||||
identifier: string;
|
||||
// callback?: any;
|
||||
// limit?: number;
|
||||
@ -102,6 +102,15 @@ export interface NostrEvent extends Event {
|
||||
tagsCut: boolean;
|
||||
}
|
||||
|
||||
export interface NostrArticle extends NostrEvent {
|
||||
slug?: string;
|
||||
title?: string;
|
||||
summary?: string;
|
||||
image?: string;
|
||||
published_at: number;
|
||||
metatags: string[];
|
||||
}
|
||||
|
||||
export interface NostrSub extends Sub {
|
||||
// id: string;
|
||||
}
|
||||
|
@ -129,7 +129,11 @@ export class NavigationService {
|
||||
event.tags.push(['image', blog.image]);
|
||||
}
|
||||
|
||||
event.tags.push(['published_at', event.created_at.toString()]);
|
||||
if (!blog.published_at) {
|
||||
event.tags.push(['published_at', event.created_at.toString()]);
|
||||
} else {
|
||||
event.tags.push(['published_at', blog.published_at.toString()]);
|
||||
}
|
||||
|
||||
const tags = blog.tags.split(',').filter((t) => t);
|
||||
|
||||
@ -144,7 +148,7 @@ export class NavigationService {
|
||||
|
||||
await this.dataService.publishEvent(signedEvent);
|
||||
|
||||
this.router.navigate(['/a', signedEvent.id]);
|
||||
// this.router.navigate(['/a', signedEvent.id]);
|
||||
}
|
||||
|
||||
createNote(): void {
|
||||
|
@ -21,17 +21,23 @@ export class QueueService {
|
||||
this.#queuesChanged.next({ identifier: identifier, type: 'Event' });
|
||||
}
|
||||
|
||||
enqueArticle(identifier: string) {
|
||||
this.#queuesChanged.next({ identifier: identifier, type: 'Article' });
|
||||
}
|
||||
|
||||
enqueContacts(identifier: string) {
|
||||
this.#queuesChanged.next({ identifier: identifier, type: 'Contacts' });
|
||||
}
|
||||
|
||||
enque(identifier: string, type: 'Profile' | 'Event' | 'Contacts') {
|
||||
enque(identifier: string, type: 'Profile' | 'Event' | 'Contacts' | 'Article') {
|
||||
if (type === 'Profile') {
|
||||
this.enqueProfile(identifier);
|
||||
} else if (type === 'Event') {
|
||||
this.enqueEvent(identifier);
|
||||
} else if (type === 'Contacts') {
|
||||
this.enqueContacts(identifier);
|
||||
} else if (type === 'Article') {
|
||||
this.enqueArticle(identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,10 @@ export class Queue {
|
||||
active: false,
|
||||
jobs: [] as QueryJob[],
|
||||
},
|
||||
article: {
|
||||
active: false,
|
||||
jobs: [] as QueryJob[],
|
||||
},
|
||||
contacts: {
|
||||
active: false,
|
||||
jobs: [] as QueryJob[],
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { LoadMoreOptions, NostrEventDocument, NostrRelay, NostrRelayDocument, NostrRelaySubscription, ProfileStatus, QueryJob } from './interfaces';
|
||||
import { LoadMoreOptions, NostrArticle, NostrEventDocument, NostrRelay, NostrRelayDocument, NostrRelaySubscription, ProfileStatus, QueryJob } from './interfaces';
|
||||
import { Observable, BehaviorSubject, from, merge, timeout, catchError, of, finalize, tap } from 'rxjs';
|
||||
import { Filter, Kind, Relay, relayInit, Sub } from 'nostr-tools';
|
||||
import { EventService } from './event';
|
||||
@ -16,6 +16,7 @@ import { ImportSheet } from '../shared/import-sheet/import-sheet';
|
||||
import { QueueService } from './queue.service';
|
||||
import { UIService } from './ui';
|
||||
import { NostrService } from './nostr';
|
||||
import { ArticleService } from './article';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@ -55,6 +56,7 @@ export class RelayService {
|
||||
}
|
||||
|
||||
constructor(
|
||||
private articleService: ArticleService,
|
||||
private nostr: NostrService,
|
||||
private ui: UIService,
|
||||
private queue: QueueService,
|
||||
@ -473,7 +475,9 @@ export class RelayService {
|
||||
}
|
||||
}
|
||||
|
||||
if (event.kind == Kind.Metadata) {
|
||||
if (event.kind == Kind.Article) {
|
||||
this.articleService.put(event);
|
||||
} else if (event.kind == Kind.Metadata) {
|
||||
// This is a profile event, store it.
|
||||
const nostrProfileDocument = this.utilities.mapProfileEvent(event);
|
||||
|
||||
|
@ -52,6 +52,8 @@ export class RelayWorker {
|
||||
this.queue.queues.contacts.jobs.push(job);
|
||||
} else if (job.type == 'Event') {
|
||||
this.queue.queues.event.jobs.push(job);
|
||||
} else if (job.type == 'Article') {
|
||||
this.queue.queues.article.jobs.push(job);
|
||||
} else {
|
||||
throw Error(`This type of job (${job.type}) is currently not supported.`);
|
||||
}
|
||||
@ -61,10 +63,11 @@ export class RelayWorker {
|
||||
// We always delay the processing in case we receive more.
|
||||
setTimeout(() => {
|
||||
this.process();
|
||||
}, 150);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
process() {
|
||||
this.processArticle();
|
||||
this.processProfiles();
|
||||
this.processContacts();
|
||||
this.processEvents();
|
||||
@ -135,7 +138,7 @@ export class RelayWorker {
|
||||
|
||||
processEvents() {
|
||||
if (!this.relay || this.relay.status != 1 || this.queue.queues.event.active) {
|
||||
console.log(`${this.url}: processProfiles: Relay not ready or currently active: ${this.queue.queues.event.active}.`, this.relay);
|
||||
console.log(`${this.url}: processEvents: Relay not ready or currently active: ${this.queue.queues.event.active}.`, this.relay);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -159,6 +162,32 @@ export class RelayWorker {
|
||||
this.downloadEvent(eventsToDownload, eventsToDownload.length * 3);
|
||||
}
|
||||
|
||||
processArticle() {
|
||||
if (!this.relay || this.relay.status != 1 || this.queue.queues.article.active) {
|
||||
console.log(`${this.url}: processArticle: Relay not ready or currently active: ${this.queue.queues.article.active}.`, this.relay);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`${this.url}: processArticle: Processing with downloading... Count: ` + this.queue.queues.article.jobs.length);
|
||||
|
||||
if (this.queue.queues.article.jobs.length == 0) {
|
||||
this.queue.queues.article.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.queue.queues.article.active = true;
|
||||
|
||||
console.log(this.relay);
|
||||
|
||||
const eventsToDownload = this.queue.queues.article.jobs
|
||||
.splice(0, 500)
|
||||
.map((j) => j.identifier)
|
||||
.filter((v, i, a) => a.indexOf(v) === i); // Unique, it can happen that multiple of same is added.
|
||||
|
||||
console.log('articleToDownload:', eventsToDownload);
|
||||
this.downloadArticle(eventsToDownload, eventsToDownload.length * 3);
|
||||
}
|
||||
|
||||
/** Provide event to publish and terminate immediately. */
|
||||
async connect(event?: any) {
|
||||
// const relay = relayInit('wss://relay.nostr.info');
|
||||
@ -250,6 +279,9 @@ export class RelayWorker {
|
||||
eventSub?: NostrSub;
|
||||
eventTimer?: any;
|
||||
|
||||
articleSub?: NostrSub;
|
||||
articleTimer?: any;
|
||||
|
||||
clearProfileSub() {
|
||||
this.profileSub?.unsub();
|
||||
this.profileSub = undefined;
|
||||
@ -265,6 +297,11 @@ export class RelayWorker {
|
||||
this.eventSub = undefined;
|
||||
}
|
||||
|
||||
clearArticleSub() {
|
||||
this.articleSub?.unsub();
|
||||
this.articleTimer = undefined;
|
||||
}
|
||||
|
||||
downloadProfile(pubkeys: string[], timeoutSeconds: number = 12) {
|
||||
console.log('DOWNLOAD PROFILE....');
|
||||
let finalizedCalled = false;
|
||||
@ -387,6 +424,64 @@ export class RelayWorker {
|
||||
}, timeoutSeconds * 1000);
|
||||
}
|
||||
|
||||
downloadArticle(ids: string[], timeoutSeconds: number = 12) {
|
||||
console.log('DOWNLOAD ARTICLE....');
|
||||
let finalizedCalled = false;
|
||||
|
||||
if (!this.relay) {
|
||||
debugger;
|
||||
console.warn('This relay does not have active connection and download cannot be executed at this time.');
|
||||
return;
|
||||
}
|
||||
|
||||
// If the profilesub already exists, unsub and remove.
|
||||
if (this.articleSub) {
|
||||
console.log('Article sub already existed, unsub before continue.');
|
||||
this.clearArticleSub();
|
||||
}
|
||||
|
||||
// Skip if the subscription is already added.
|
||||
// if (this.subscriptions.findIndex((s) => s.id == id) > -1) {
|
||||
// debugger;
|
||||
// console.log('This subscription is already added!');
|
||||
// return;
|
||||
// }
|
||||
|
||||
const filter = { kinds: [Kind.Article], authors: ids };
|
||||
const sub = this.relay.sub([filter]) as NostrSub;
|
||||
this.articleSub = sub;
|
||||
|
||||
sub.on('event', (originalEvent: any) => {
|
||||
console.log('POST MESSAGE BACK TO MAIN');
|
||||
postMessage({ url: this.url, type: 'event', data: originalEvent } as RelayResponse);
|
||||
console.log('FINISHED POST MESSAGE BACK TO MAIN');
|
||||
});
|
||||
|
||||
sub.on('eose', () => {
|
||||
console.log('eose on event.');
|
||||
clearTimeout(this.articleTimer);
|
||||
this.clearArticleSub();
|
||||
this.queue.queues.article.active = false;
|
||||
this.processArticle();
|
||||
});
|
||||
|
||||
console.log('REGISTER TIMEOUT!!', timeoutSeconds * 1000);
|
||||
|
||||
this.articleTimer = setTimeout(() => {
|
||||
console.warn(`${this.url}: Event download timeout reached.`);
|
||||
this.clearArticleSub();
|
||||
this.queue.queues.article.active = false;
|
||||
this.processArticle();
|
||||
|
||||
postMessage({ url: this.url, type: 'timeout', data: { type: 'Event', identifier: ids } } as RelayResponse);
|
||||
|
||||
// if (!finalizedCalled) {
|
||||
// finalizedCalled = true;
|
||||
// finalized();
|
||||
// }
|
||||
}, timeoutSeconds * 1000);
|
||||
}
|
||||
|
||||
downloadEvent(ids: string[], timeoutSeconds: number = 12) {
|
||||
console.log('DOWNLOAD EVENT....');
|
||||
let finalizedCalled = false;
|
||||
|
Loading…
Reference in New Issue
Block a user