mirror of
https://github.com/block-core/blockcore-notes.git
synced 2024-09-29 06:20:42 +00:00
Feat: add Zap Notes & display each post zappers
This commit is contained in:
parent
697c092e91
commit
b0735299f3
17
package-lock.json
generated
17
package-lock.json
generated
@ -36,6 +36,7 @@
|
||||
"html5-qrcode": "^2.3.7",
|
||||
"idb": "^7.1.1",
|
||||
"joi": "^17.8.3",
|
||||
"light-bolt11-decoder": "^3.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"ngx-colors": "^3.5.0",
|
||||
"ngx-drag-scroll": "^15.0.0",
|
||||
@ -8404,6 +8405,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/light-bolt11-decoder": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.0.0.tgz",
|
||||
"integrity": "sha512-AKvOigD2pmC8ktnn2TIqdJu0K0qk6ukUmTvHwF3JNkm8uWCqt18Ijn33A/a7gaRZ4PghJ59X+8+MXrzLKdBTmQ==",
|
||||
"dependencies": {
|
||||
"@scure/base": "1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lines-and-columns": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
@ -19210,6 +19219,14 @@
|
||||
"webpack-sources": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"light-bolt11-decoder": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.0.0.tgz",
|
||||
"integrity": "sha512-AKvOigD2pmC8ktnn2TIqdJu0K0qk6ukUmTvHwF3JNkm8uWCqt18Ijn33A/a7gaRZ4PghJ59X+8+MXrzLKdBTmQ==",
|
||||
"requires": {
|
||||
"@scure/base": "1.1.1"
|
||||
}
|
||||
},
|
||||
"lines-and-columns": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"html5-qrcode": "^2.3.7",
|
||||
"idb": "^7.1.1",
|
||||
"joi": "^17.8.3",
|
||||
"light-bolt11-decoder": "^3.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"ngx-colors": "^3.5.0",
|
||||
"ngx-drag-scroll": "^15.0.0",
|
||||
|
@ -153,6 +153,7 @@ import { TagsComponent } from './shared/tags/tags';
|
||||
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';
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
@ -243,6 +244,7 @@ import { DragScrollModule } from 'ngx-drag-scroll';
|
||||
BadgeCardComponent,
|
||||
TagsComponent,
|
||||
BadgeComponent,
|
||||
ZappersListDialogComponent,
|
||||
],
|
||||
imports: [
|
||||
AboutModule,
|
||||
|
@ -169,6 +169,29 @@ export interface ThreadEntry {
|
||||
reactions: { [key: string | symbol]: number };
|
||||
// reactions: {};
|
||||
boosts: number;
|
||||
zaps?: ParsedZap[];
|
||||
}
|
||||
|
||||
export interface Zapper {
|
||||
pubkey?: string;
|
||||
isValid: boolean;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ZappersListData {
|
||||
zaps: ParsedZap[] | undefined;
|
||||
event?: NostrEventDocument;
|
||||
}
|
||||
|
||||
export interface ParsedZap {
|
||||
id: string;
|
||||
e?: string;
|
||||
p: string;
|
||||
amount: number;
|
||||
content: string;
|
||||
zapper?: string;
|
||||
valid: boolean;
|
||||
zapService: string;
|
||||
}
|
||||
|
||||
export enum EmojiEnum {
|
||||
|
@ -6,13 +6,14 @@ import { EventService } from './event';
|
||||
import { EmojiEnum, LoadMoreOptions, NostrEvent, NostrEventDocument, NostrProfileDocument, NotificationModel, ThreadEntry } from './interfaces';
|
||||
import { OptionsService } from './options';
|
||||
import { ProfileService } from './profile';
|
||||
import { ZapService } from './zap.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
/** The orchestrator for UI that holds data to be rendered in different views at any given time. */
|
||||
export class UIService {
|
||||
constructor(private eventService: EventService, private options: OptionsService) {}
|
||||
constructor(private eventService: EventService, private options: OptionsService, private zapService: ZapService) { }
|
||||
|
||||
#lists = {
|
||||
feedEvents: [] as NostrEventDocument[],
|
||||
@ -600,6 +601,22 @@ export class UIService {
|
||||
this.#viewEventsChanged.next(this.viewEvents);
|
||||
this.#viewReplyEventsChanged.next(this.viewReplyEvents);
|
||||
}
|
||||
} else if (event.kind == Kind.Zap) {
|
||||
const eventId = this.eventService.lastETag(event);
|
||||
|
||||
if (eventId) {
|
||||
const entry = this.getThreadEntry(eventId);
|
||||
|
||||
if (entry.reactionIds.includes(event.id!)) {
|
||||
return;
|
||||
}
|
||||
|
||||
entry.reactionIds.push(event.id!);
|
||||
const parsedZap = this.zapService.parseZap(event);
|
||||
entry.zaps != undefined ? entry.zaps.push(parsedZap) : entry.zaps = [parsedZap];
|
||||
|
||||
this.putThreadEntry(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
102
src/app/services/zap.service.ts
Normal file
102
src/app/services/zap.service.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { NostrEventDocument, Zapper, ParsedZap } from "./interfaces";
|
||||
var lightningPayReq = require('light-bolt11-decoder')
|
||||
import { sha256 } from "@noble/hashes/sha256";
|
||||
import { Utilities } from './utilities';
|
||||
import { ProfileService } from './profile';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ZapService {
|
||||
|
||||
|
||||
constructor(private util: Utilities) { }
|
||||
|
||||
|
||||
parseZap(zapEvent: NostrEventDocument): ParsedZap {
|
||||
const { amount, hash } = this.getInvoice(zapEvent);
|
||||
const zapper = hash ? this.getZapper(zapEvent, hash) : ({ isValid: false, content: "" } as Zapper);
|
||||
const e = this.zappedAuthor(zapEvent);
|
||||
const p = this.zappedPost(zapEvent);
|
||||
debugger
|
||||
|
||||
return {
|
||||
id: zapEvent.id,
|
||||
e: e != undefined ? e : undefined,
|
||||
p: p != undefined ? p : "",
|
||||
amount: Number(amount) / 1000,
|
||||
zapper: zapper?.pubkey,
|
||||
content: zapper?.content,
|
||||
valid: zapper?.isValid,
|
||||
zapService: zapEvent.pubkey
|
||||
}
|
||||
}
|
||||
|
||||
getInvoice(event: NostrEventDocument) {
|
||||
const bolt11 = this.lnInvoice(event)
|
||||
if (!bolt11) {
|
||||
console.error('Invalid zap: No bolt11 tag found in event', event);
|
||||
return {}
|
||||
}
|
||||
|
||||
try {
|
||||
const decodedBolt11 = lightningPayReq.decode(bolt11);
|
||||
|
||||
const amount = decodedBolt11.sections.find((s: any) => s.name === 'amount')?.value;
|
||||
const hash = decodedBolt11.sections.find((s: any) => s.name === 'description_hash')?.value;
|
||||
return { amount, hash };
|
||||
} catch (error) {
|
||||
console.error('Invalid zap: Could not decode bolt11', event);
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
getZapper(zapEvent: any, hash: any): Zapper {
|
||||
debugger
|
||||
let zapRequest = this.description(zapEvent);
|
||||
if (zapRequest) {
|
||||
try {
|
||||
if (zapRequest.startsWith("%")) {
|
||||
zapRequest = decodeURIComponent(zapRequest);
|
||||
}
|
||||
|
||||
const rawZapRequest = JSON.parse(zapRequest);
|
||||
const eventHash = this.util.arrayToHex(sha256(zapRequest));
|
||||
return { pubkey: rawZapRequest.pubkey, isValid: hash === eventHash, content: rawZapRequest.content }
|
||||
} catch (error) {
|
||||
console.error('Invalid zap: Could not decode zap request', zapEvent);
|
||||
}
|
||||
}
|
||||
return { isValid: false, content: "" }
|
||||
}
|
||||
|
||||
private lnInvoice(event: NostrEventDocument): string | null {
|
||||
return event.tags
|
||||
.filter((tag: Array<string>) => tag[0] === "bolt11")
|
||||
.map((tag: Array<string>) => tag[1])
|
||||
.find((invoice: string | undefined) => invoice != null)
|
||||
|| null
|
||||
}
|
||||
public zappedPost(event: NostrEventDocument): string | null {
|
||||
return event.tags
|
||||
.filter((tag) => tag[0] === 'e')
|
||||
.map((tag) => tag[1] ?? null)
|
||||
.find((post) => post != null) ?? null;
|
||||
}
|
||||
|
||||
public zappedAuthor(event: NostrEventDocument): string | null {
|
||||
return event.tags
|
||||
.filter((tag) => tag[0] === 'p')
|
||||
.map((tag) => tag[1] ?? null)
|
||||
.find((author) => author != null) ?? null;
|
||||
}
|
||||
private description(event: NostrEventDocument): string | null {
|
||||
return event.tags
|
||||
.filter((tag) => tag[0] === 'description')
|
||||
.map((tag) => tag[1] ?? null)
|
||||
.find((description) => description != null) ?? null;
|
||||
}
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
<div class="thread-actions" [ngClass]="{ 'no-lines': !optionsService.values.showLines, 'lines': optionsService.values.showLines }">
|
||||
<ng-template [ngIf]="!replyOpen">
|
||||
<mat-icon class="reaction-icon toolbar-icon" style="margin-right: 10px;" (click)="openDialog()" matTooltip="Reaction to post">offline_bolt</mat-icon>
|
||||
<mat-icon class="reaction-icon toolbar-icon" (click)="isEmojiPickerVisible = !isEmojiPickerVisible;" matTooltip="Reaction to post">sentiment_satisfied</mat-icon>
|
||||
<emoji-mart class="picker" *ngIf="isEmojiPickerVisible" emoji="point_up" [isNative]="true" [showPreview]="true" (emojiSelect)="addEmoji($event)" title="Choose your reaction"></emoji-mart>
|
||||
|
||||
<button (click)="openReply()" mat-button>Reply</button>
|
||||
</ng-template>
|
||||
<!-- <button mat-button>
|
||||
|
@ -9,6 +9,7 @@ import { ProfileService } from 'src/app/services/profile';
|
||||
import { Utilities } from 'src/app/services/utilities';
|
||||
import { NostrEventDocument, NostrProfile, NostrProfileDocument } from '../../services/interfaces';
|
||||
import { ProfileImageDialog } from '../profile-image-dialog/profile-image-dialog';
|
||||
import { ZapDialogComponent } from '../zap-dialog/zap-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-event-buttons',
|
||||
@ -32,10 +33,16 @@ export class EventButtonsComponent {
|
||||
replyOpen = false;
|
||||
publishing = false;
|
||||
error = '';
|
||||
profile?: NostrProfileDocument;
|
||||
|
||||
@ViewChild('replyInput') replyInput?: ElementRef;
|
||||
|
||||
constructor(private eventService: EventService, private dataService: DataService, public optionsService: OptionsService, private profileService: ProfileService, private utilities: Utilities, public dialog: MatDialog) {}
|
||||
constructor(private eventService: EventService, private dataService: DataService, public optionsService: OptionsService, private profileService: ProfileService, private utilities: Utilities, public dialog: MatDialog) { }
|
||||
|
||||
async ngAfterViewInit() {
|
||||
let pubkey = this.event?.pubkey ? this.event?.pubkey : "";
|
||||
this.profile = await this.profileService.getProfile(pubkey);
|
||||
}
|
||||
|
||||
openReply() {
|
||||
this.replyOpen = true;
|
||||
@ -127,4 +134,14 @@ export class EventButtonsComponent {
|
||||
this.publishing = false;
|
||||
}
|
||||
}
|
||||
|
||||
async openDialog() {
|
||||
this.dialog.open(ZapDialogComponent, {
|
||||
width: '400px',
|
||||
data: {
|
||||
profile: this.profile,
|
||||
event: this.event,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
<div class="thread-reactions" [ngClass]="{ 'no-lines': !optionsService.values.showLines, lines: optionsService.values.showLines }" *ngIf="threadEntry?.boosts || threadEntry?.reactions">
|
||||
<span class="thread-reaction" *ngIf="threadEntry?.boosts">🔁 {{ threadEntry?.boosts }}</span>
|
||||
<span class="thread-reaction" style="cursor: pointer;" (click)="openDialog()" *ngIf="threadEntry?.zaps"> ⚡️ {{ amountZapped }} </span>
|
||||
<span class="thread-reaction" *ngFor="let item of threadEntry!.reactions | keyvalue"> {{ item.key }} {{ item.value }} </span>
|
||||
</div>
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { OptionsService } from 'src/app/services/options';
|
||||
import { ThreadService } from 'src/app/services/thread';
|
||||
import { Circle, NostrEventDocument, ThreadEntry } from '../../services/interfaces';
|
||||
import { Circle, NostrEventDocument, NostrProfileDocument, ThreadEntry } from '../../services/interfaces';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { UIService } from 'src/app/services/ui';
|
||||
import { ProfileService } from 'src/app/services/profile';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ZappersListDialogComponent } from '../zappers-list-dialog/zappers-list-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-event-reactions',
|
||||
@ -21,10 +24,11 @@ export class EventReactionsComponent {
|
||||
profileName = '';
|
||||
circle?: Circle;
|
||||
sub?: Subscription;
|
||||
amountZapped = 0;
|
||||
|
||||
constructor(public thread: ThreadService, public ui: UIService, public optionsService: OptionsService) {}
|
||||
constructor(public thread: ThreadService, private profiles: ProfileService, public ui: UIService, public optionsService: OptionsService, public dialog: MatDialog) { }
|
||||
|
||||
ngAfterViewInit() {}
|
||||
ngAfterViewInit() { }
|
||||
|
||||
async ngOnInit() {
|
||||
if (!this.optionsService.values.enableReactions) {
|
||||
@ -40,6 +44,7 @@ export class EventReactionsComponent {
|
||||
|
||||
if (id == this.event.id) {
|
||||
this.threadEntry = this.thread.getTreeEntry(this.event.id!);
|
||||
this.amountZapped = this.threadEntry?.zaps?.reduce((sum: number, zap: any) => sum + zap.amount, 0) || 0;
|
||||
}
|
||||
});
|
||||
|
||||
@ -49,4 +54,15 @@ export class EventReactionsComponent {
|
||||
ngOnDestroy() {
|
||||
this.sub?.unsubscribe();
|
||||
}
|
||||
|
||||
openDialog() {
|
||||
const zaps = this.threadEntry?.zaps;
|
||||
this.dialog.open(ZappersListDialogComponent, {
|
||||
data: {
|
||||
zaps: zaps,
|
||||
event: this.event
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -17,13 +17,31 @@
|
||||
</div>
|
||||
<div class="emoji-container">
|
||||
<div class="emoji-button">
|
||||
<button (click)="setAmount(500)" class="thumb-up-btn" mat-icon-button color="primary">
|
||||
<mat-icon>thumb_up</mat-icon><span class="emoji-number">500</span>
|
||||
<button (click)="setAmount(50)" class="thumb-up-btn" mat-icon-button color="primary">
|
||||
<mat-icon>thumb_up</mat-icon><span class="emoji-number">50</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="emoji-button">
|
||||
<button (click)="setAmount(1000)" class="favorite-btn" mat-icon-button color="primary">
|
||||
<mat-icon>favorite</mat-icon><span class="emoji-number">1k</span>
|
||||
<button (click)="setAmount(100)" class="favorite-btn" mat-icon-button color="primary">
|
||||
<mat-icon>favorite</mat-icon><span class="emoji-number">100</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="emoji-button">
|
||||
<button (click)="setAmount(500)" class="applause-btn" mat-icon-button color="primary">
|
||||
<mat-icon>sentiment_very_satisfied</mat-icon><span class="emoji-number">500</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="emoji-container">
|
||||
<div class="emoji-button">
|
||||
<button (click)="setAmount(1000)" class="thumb-up-btn" mat-icon-button color="primary">
|
||||
<mat-icon>star</mat-icon><span class="emoji-number">1k</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="emoji-button">
|
||||
<button (click)="setAmount(5000)" class="favorite-btn" mat-icon-button color="primary">
|
||||
<mat-icon>celebration</mat-icon><span class="emoji-number">5k</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="emoji-button">
|
||||
@ -32,6 +50,24 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="emoji-container">
|
||||
<div class="emoji-button">
|
||||
<button (click)="setAmount(100_000)" class="thumb-up-btn" mat-icon-button color="primary">
|
||||
<mat-icon>whatshot</mat-icon><span class="emoji-number">100k</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="emoji-button">
|
||||
<button (click)="setAmount(500_000)" class="favorite-btn" mat-icon-button color="primary">
|
||||
<mat-icon>flash_on</mat-icon><span class="emoji-number"> 500k</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="emoji-button">
|
||||
<button (click)="setAmount(1_000_000)" class="applause-btn" mat-icon-button color="primary">
|
||||
<mat-icon>diamond</mat-icon><span class="emoji-number">1M</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
@ -50,7 +50,7 @@
|
||||
margin: 0 18px;
|
||||
border-radius: 15%;
|
||||
background-color: #eee;
|
||||
width: 21%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
@ -9,10 +9,12 @@ import { NostrService } from 'src/app/services/nostr';
|
||||
import { Utilities } from 'src/app/services/utilities';
|
||||
import { DataService } from 'src/app/services/data';
|
||||
import { ZapQrCodeComponent } from '../zap-qr-code/zap-qr-code.component';
|
||||
import { NostrProfileDocument, LNURLPayRequest, LNURLInvoice, NostrEventDocument } from 'src/app/services/interfaces';
|
||||
import { NostrProfileDocument, LNURLPayRequest, LNURLInvoice, NostrEventDocument, NostrRelayDocument } from 'src/app/services/interfaces';
|
||||
import { StorageService } from 'src/app/services/storage';
|
||||
|
||||
export interface ZapDialogData {
|
||||
profile: NostrProfileDocument;
|
||||
event?: NostrEventDocument;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@ -20,7 +22,7 @@ export interface ZapDialogData {
|
||||
templateUrl: './zap-dialog.component.html',
|
||||
styleUrls: ['./zap-dialog.component.scss']
|
||||
})
|
||||
export class ZapDialogComponent implements OnInit {
|
||||
export class ZapDialogComponent implements OnInit {
|
||||
sendZapForm!: UntypedFormGroup;
|
||||
minSendable: number = 0;
|
||||
maxSendable: number = 0;
|
||||
@ -37,13 +39,15 @@ export class ZapDialogComponent implements OnInit {
|
||||
tooltipName = '';
|
||||
profileName = '';
|
||||
error: string = "";
|
||||
event?: NostrEventDocument | undefined;
|
||||
|
||||
constructor(@Inject(MAT_DIALOG_DATA) public data: ZapDialogData,
|
||||
constructor(@Inject(MAT_DIALOG_DATA) public data: ZapDialogData,
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private eventService: EventService,
|
||||
private relayService: RelayService,
|
||||
private nostr: NostrService,
|
||||
private util: Utilities,
|
||||
private db: StorageService,
|
||||
private dataService: DataService,
|
||||
private dialog: MatDialog,
|
||||
public dialogRef: MatDialogRef<ZapDialogComponent>
|
||||
@ -54,6 +58,7 @@ export class ZapDialogComponent implements OnInit {
|
||||
|
||||
async ngOnInit() {
|
||||
this.profile = this.data.profile
|
||||
this.event = this.data.event
|
||||
this.sendZapForm = this.formBuilder.group({
|
||||
amount: ['', [Validators.required]],
|
||||
comment: ['']
|
||||
@ -64,26 +69,26 @@ export class ZapDialogComponent implements OnInit {
|
||||
}
|
||||
|
||||
async fetchPayReq(): Promise<void> {
|
||||
this.payRequest = await this.fetchZapper()
|
||||
this.payRequest = await this.fetchZapper()
|
||||
this.recofigureFormValidators()
|
||||
}
|
||||
|
||||
|
||||
async fetchZapper(): Promise<LNURLPayRequest | null> {
|
||||
let staticPayReq = ""
|
||||
if(this.profile.lud16) {
|
||||
if (this.profile.lud16) {
|
||||
const parts = this.profile.lud16.split("@")
|
||||
staticPayReq = `https://${parts[1]}/.well-known/lnurlp/${parts[0]}`;
|
||||
} else if (this.profile.lud06 && this.profile.lud06.toLowerCase().startsWith("lnurl")) {
|
||||
staticPayReq = this.util.convertBech32ToText(this.profile.lud06).toString()
|
||||
}
|
||||
|
||||
if(staticPayReq.length !== 0) {
|
||||
if (staticPayReq.length !== 0) {
|
||||
try {
|
||||
const resp = await fetch(staticPayReq);
|
||||
if(resp.ok) {
|
||||
if (resp.ok) {
|
||||
const payReq = await resp.json();
|
||||
if(payReq.status === "ERROR") {
|
||||
if (payReq.status === "ERROR") {
|
||||
this.error = payReq.reason ? payReq.reason : "Error fetching the invoice - please try again later"
|
||||
} else {
|
||||
return payReq
|
||||
@ -99,39 +104,42 @@ export class ZapDialogComponent implements OnInit {
|
||||
|
||||
async onSubmit() {
|
||||
if (this.sendZapForm.valid) {
|
||||
debugger
|
||||
let comment = this.sendZapForm.get('comment')?.value
|
||||
let amount = this.sendZapForm.get('amount')?.value
|
||||
if(!amount || !this.payRequest) {
|
||||
console.log( "error: please enter an amount and a valid pay request")
|
||||
if (!amount || !this.payRequest) {
|
||||
console.log("error: please enter an amount and a valid pay request")
|
||||
} else {
|
||||
const callback = new URL(this.payRequest.callback)
|
||||
const query = new Map<string, string>()
|
||||
query.set("amount", Math.floor(amount * 1000).toString())
|
||||
if(comment && this.payRequest?.commentAllowed) {
|
||||
if (comment && this.payRequest?.commentAllowed) {
|
||||
query.set("comment", comment)
|
||||
}
|
||||
|
||||
let event;
|
||||
if(this.payRequest.nostrPubkey)
|
||||
if(this.profile.pubkey) {
|
||||
event = await this.createZapEvent(this.profile.pubkey, null, comment);
|
||||
query.set("nostr", JSON.stringify(event))
|
||||
}
|
||||
|
||||
|
||||
let zapReqEvent;
|
||||
if (this.payRequest.nostrPubkey)
|
||||
if (this.profile.pubkey) {
|
||||
debugger
|
||||
let note = this.event?.id ? this.event.id : null
|
||||
zapReqEvent = await this.createZapEvent(this.profile.pubkey, note, comment);
|
||||
query.set("nostr", JSON.stringify(zapReqEvent))
|
||||
}
|
||||
|
||||
const baseUrl = `${callback.protocol}//${callback.host}${callback.pathname}`;
|
||||
const queryJoined = [...query.entries()].map(val => `${val[0]}=${encodeURIComponent(val[1])}`).join("&");
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}?${queryJoined}`)
|
||||
if(response.ok) {
|
||||
if (response.ok) {
|
||||
const result = await response.json()
|
||||
if(result.status === "ERROR") {
|
||||
if (result.status === "ERROR") {
|
||||
this.error = result.reason ? result.reason : "Error fetching the invoice - please try again later"
|
||||
} else {
|
||||
this.invoice = result
|
||||
this.dialog.open(ZapQrCodeComponent, {
|
||||
width: '400px',
|
||||
data: {
|
||||
data: {
|
||||
invoice: this.invoice,
|
||||
profile: this.profile
|
||||
}
|
||||
@ -147,27 +155,37 @@ export class ZapDialogComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async createZapEvent(targetPubKey: string, note?: any, msg?: string) {
|
||||
let zapEvent = this.dataService.createEvent(Kind.Zap, '');
|
||||
items: NostrRelayDocument[] = [];
|
||||
|
||||
async createZapEvent(targetPubKey: string, note?: any, msg?: string) {
|
||||
let zapEvent = this.dataService.createEvent(Kind.ZapRequest, '');
|
||||
|
||||
zapEvent.content = msg ? msg : '';
|
||||
Object.assign([], zapEvent.tags);
|
||||
if(note) {
|
||||
if (note) {
|
||||
zapEvent.tags.push(['e', note]);
|
||||
}
|
||||
zapEvent.tags.push(['p', targetPubKey]);
|
||||
zapEvent.tags.push(['relays', ...Object.keys(this.relayService.relays)]);
|
||||
|
||||
// zapEvent.tags.push(['relays', ...Object.keys(this.relayService)]);
|
||||
zapEvent = await this.addRelaysTag(zapEvent);
|
||||
const signedEvent = await this.createAndSignEvent(zapEvent);
|
||||
|
||||
|
||||
if (!signedEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
return signedEvent;
|
||||
|
||||
|
||||
}
|
||||
|
||||
async addRelaysTag(zapEvent: UnsignedEvent) {
|
||||
debugger
|
||||
this.items = await this.db.storage.getRelays();
|
||||
const relays = this.items.map((item) => item.url);
|
||||
zapEvent.tags.push(['relays', ...relays]);
|
||||
return zapEvent;
|
||||
}
|
||||
private async createAndSignEvent(originalEvent: UnsignedEvent) {
|
||||
let signedEvent = originalEvent as Event;
|
||||
|
||||
@ -199,8 +217,8 @@ export class ZapDialogComponent implements OnInit {
|
||||
this.minSendable = (this.payRequest?.minSendable || 1000) / 1000
|
||||
this.maxSendable = (this.payRequest?.maxSendable || 21_000_000_000) / 1000
|
||||
this.sendZapForm.get('amount')?.setValidators([
|
||||
Validators.min((this.payRequest?.minSendable || 1000) / 1000),
|
||||
Validators.max((this.payRequest?.maxSendable || 21_000_000_000) / 1000),
|
||||
Validators.min((this.payRequest?.minSendable || 1000) / 1000),
|
||||
Validators.max((this.payRequest?.maxSendable || 21_000_000_000) / 1000),
|
||||
Validators.required
|
||||
])
|
||||
}
|
||||
|
@ -0,0 +1,29 @@
|
||||
<div mat-dialog-content>
|
||||
<h2 style="text-align: center; margin-bottom: 25px">Zappers</h2>
|
||||
<div class="profile-container" *ngFor="let zapper of zappers">
|
||||
<div class="left" style="display: flex; align-items: center; margin-right: 40px;">
|
||||
<div class="icon icon-small" style="flex: 0 0 auto; margin-right: 8px;">
|
||||
<img
|
||||
onerror="this.src='/assets/profile.png'"
|
||||
*ngIf="zapper.profile?.status == 1 || profile?.status == 2"
|
||||
class="profile-image profile-image-follow"
|
||||
[matTooltip]="zapper.profile.about"
|
||||
matTooltipPosition="above"
|
||||
[src]="zapper.profile.picture"
|
||||
/>
|
||||
<img loading="lazy" onerror="this.src='/assets/profile.png'" *ngIf="zapper.profile?.status != 1 && zapper.profile?.status != 2" class="profile-image" [matTooltip]="zapper.profile.about" matTooltipPosition="above" [src]="zapper.profile.picture" />
|
||||
</div>
|
||||
<div style="flex: 1 1 auto;">
|
||||
<div class="name clickable">
|
||||
<span [class.muted]="zapper.profile.status == 2" [matTooltip]="zapper.profile.display_name" matTooltipPosition="above">{{ zapper.profile.display_name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span class="zap-amount" matTooltipPosition="above"> ⚡️ {{ zapper.zap.amount }}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -0,0 +1,114 @@
|
||||
.mat-mdc-form-field {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.mat-mdc-dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.zap-button {
|
||||
background-color: #9c27b0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.profile-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.profile-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border: unset;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.emoji-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.emoji-button {
|
||||
margin: 0 18px;
|
||||
border-radius: 15%;
|
||||
background-color: #eee;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.favorite-btn,
|
||||
.thumb-up-btn,
|
||||
.applause-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// .favorite-btn:hover,
|
||||
// .thumb-up-btn:hover,
|
||||
// .applause-btn:hover
|
||||
.emoji-button:hover {
|
||||
background-color: #9c27b0;
|
||||
}
|
||||
|
||||
.mat-mdc-icon-button.mat-primary:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.favorite-btn .emoji-number,
|
||||
.thumb-up-btn .emoji-number,
|
||||
.applause-btn .emoji-number {
|
||||
position: absolute;
|
||||
right: -15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.mat-mdc-icon-button.mat-primary {
|
||||
--mat-mdc-button-persistent-ripple-color: rgba(255, 255, 255, 0);
|
||||
--mat-mdc-button-ripple-color: rgba(255, 255, 255, 0);
|
||||
}
|
||||
|
||||
|
||||
.profile-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.left {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.right {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.zap-amount {
|
||||
display: inline-block;
|
||||
border: 2px solid #9c27b0; /* blue border */
|
||||
border-radius: 10px; /* rounded corners */
|
||||
padding: 10px 15px; /* add some padding */
|
||||
font-weight: bold; /* make font bold */
|
||||
color: #fff; /* white text color */
|
||||
background-color: #9c27b0; /* blue background color */
|
||||
box-shadow: 2px 2px 5px rgba(0,0,0,0.3); /* add a subtle box shadow */
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ZappersListDialogComponent } from './zappers-list-dialog.component';
|
||||
|
||||
describe('ZappersListDialogComponent', () => {
|
||||
let component: ZappersListDialogComponent;
|
||||
let fixture: ComponentFixture<ZappersListDialogComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ ZappersListDialogComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ZappersListDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,62 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { UntypedFormGroup } from '@angular/forms';
|
||||
import { Utilities } from 'src/app/services/utilities';
|
||||
import { NostrProfileDocument, LNURLPayRequest, LNURLInvoice, NostrEventDocument, ParsedZap, ZappersListData } from 'src/app/services/interfaces';
|
||||
import { ProfileService } from 'src/app/services/profile';
|
||||
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-zappers-list-dialog',
|
||||
templateUrl: './zappers-list-dialog.component.html',
|
||||
styleUrls: ['./zappers-list-dialog.component.scss']
|
||||
})
|
||||
export class ZappersListDialogComponent {
|
||||
sendZapForm!: UntypedFormGroup;
|
||||
minSendable: number = 0;
|
||||
maxSendable: number = 0;
|
||||
profile!: NostrProfileDocument
|
||||
amount: number = 0;
|
||||
comment = "";
|
||||
payRequest: LNURLPayRequest | null = null
|
||||
invoice: LNURLInvoice = {
|
||||
pr: ""
|
||||
}
|
||||
|
||||
imagePath = '/assets/profile.png';
|
||||
tooltip = '';
|
||||
tooltipName = '';
|
||||
profileName = '';
|
||||
error: string = "";
|
||||
event?: NostrEventDocument | undefined;
|
||||
zappers: Zapper[] = [];
|
||||
constructor(@Inject(MAT_DIALOG_DATA) public data: ZappersListData,
|
||||
private util: Utilities,
|
||||
private profileServ: ProfileService,
|
||||
public dialogRef: MatDialogRef<ZappersListDialogComponent>
|
||||
) {
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
debugger
|
||||
const zaps = this.data.zaps ? this.data.zaps : [];
|
||||
zaps.forEach(async (zap: ParsedZap) => {
|
||||
let zapper = zap.zapper;
|
||||
if (zapper !== undefined && zapper !== null) {
|
||||
this.profileServ.getProfile(zapper).then(async (profile) => {
|
||||
profile.display_name = profile.display_name || profile.name || this.util.getNostrIdentifier(profile.pubkey);
|
||||
this.zappers.push({ profile: profile, zap: zap });
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
interface Zapper {
|
||||
profile: NostrProfileDocument;
|
||||
zap: ParsedZap;
|
||||
}
|
Loading…
Reference in New Issue
Block a user