Feat: add Zap Notes & display each post zappers

This commit is contained in:
jrakibi 2023-03-12 21:13:48 +01:00
parent 697c092e91
commit b0735299f3
17 changed files with 523 additions and 43 deletions

17
package-lock.json generated
View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -50,7 +50,7 @@
margin: 0 18px;
border-radius: 15%;
background-color: #eee;
width: 21%;
width: 100%;
height: 100%;
}

View File

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

View File

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

View File

@ -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 */
}

View File

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

View File

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