clndash: zap rendering

Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
William Casarin
2025-08-10 17:46:09 -07:00
parent f77e7898b6
commit 2fde5addeb
4 changed files with 129 additions and 14 deletions

6
Cargo.lock generated
View File

@@ -2370,6 +2370,9 @@ name = "hex"
version = "0.4.3" version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "hex-conservative" name = "hex-conservative"
@@ -3590,9 +3593,12 @@ dependencies = [
"eframe", "eframe",
"egui", "egui",
"egui_extras", "egui_extras",
"hex",
"lightning-invoice", "lightning-invoice",
"lnsocket", "lnsocket",
"nostrdb",
"notedeck", "notedeck",
"notedeck_ui",
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",

View File

@@ -37,7 +37,7 @@ ewebsock = { version = "0.2.0", features = ["tls"] }
fluent = "0.17.0" fluent = "0.17.0"
fluent-resmgr = "0.0.8" fluent-resmgr = "0.0.8"
fluent-langneg = "0.13" fluent-langneg = "0.13"
hex = "0.4.3" hex = { version = "0.4.3", features = ["serde"] }
image = { version = "0.25", features = ["jpeg", "png", "webp"] } image = { version = "0.25", features = ["jpeg", "png", "webp"] }
indexmap = "2.6.0" indexmap = "2.6.0"
log = "0.4.17" log = "0.4.17"

View File

@@ -14,5 +14,8 @@ tokio = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
egui_extras = { workspace = true } egui_extras = { workspace = true }
lightning-invoice = { workspace = true } lightning-invoice = { workspace = true }
hex = { workspace = true }
nostrdb = { workspace = true }
notedeck_ui = { workspace = true }
lnsocket = "0.5.1" lnsocket = "0.5.1"

View File

@@ -7,11 +7,13 @@ use crate::event::ListPeerChannel;
use crate::event::Request; use crate::event::Request;
use crate::watch::fetch_paid_invoices; use crate::watch::fetch_paid_invoices;
use egui::{Color32, Label, RichText}; use egui::{Color32, Label, RichText, Widget};
use lnsocket::bitcoin::secp256k1::{PublicKey, SecretKey, rand}; use lnsocket::bitcoin::secp256k1::{PublicKey, SecretKey, rand};
use lnsocket::{CommandoClient, LNSocket}; use lnsocket::{CommandoClient, LNSocket};
use nostrdb::Ndb;
use notedeck::{AppAction, AppContext}; use notedeck::{AppAction, AppContext};
use serde_json::json; use serde_json::json;
use std::collections::HashMap;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel}; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
@@ -65,9 +67,17 @@ pub struct ClnDash {
summary: LoadingState<Summary, lnsocket::Error>, summary: LoadingState<Summary, lnsocket::Error>,
get_info: LoadingState<String, lnsocket::Error>, get_info: LoadingState<String, lnsocket::Error>,
channels: LoadingState<Channels, lnsocket::Error>, channels: LoadingState<Channels, lnsocket::Error>,
channel: Option<CommChannel>,
invoices: LoadingState<Vec<Invoice>, lnsocket::Error>, invoices: LoadingState<Vec<Invoice>, lnsocket::Error>,
channel: Option<CommChannel>,
last_summary: Option<Summary>, last_summary: Option<Summary>,
// invoice label to zapreq id
invoice_zap_reqs: HashMap<String, [u8; 32]>,
}
#[derive(serde::Deserialize)]
pub struct ZapReqId {
#[serde(with = "hex::serde")]
id: [u8; 32],
} }
impl Default for ConnectionState { impl Default for ConnectionState {
@@ -88,7 +98,7 @@ enum ConnectionState {
} }
impl notedeck::App for ClnDash { impl notedeck::App for ClnDash {
fn update(&mut self, _ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> { fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> {
if !self.initialized { if !self.initialized {
self.connection_state = ConnectionState::Connecting; self.connection_state = ConnectionState::Connecting;
@@ -96,9 +106,9 @@ impl notedeck::App for ClnDash {
self.initialized = true; self.initialized = true;
} }
self.process_events(); self.process_events(ctx.ndb);
self.show(ui); self.show(ui, ctx);
None None
} }
@@ -144,14 +154,14 @@ fn summary_ui(
} }
impl ClnDash { impl ClnDash {
fn show(&mut self, ui: &mut egui::Ui) { fn show(&mut self, ui: &mut egui::Ui, ctx: &mut AppContext) {
egui::Frame::new() egui::Frame::new()
.inner_margin(egui::Margin::same(20)) .inner_margin(egui::Margin::same(20))
.show(ui, |ui| { .show(ui, |ui| {
egui::ScrollArea::vertical().show(ui, |ui| { egui::ScrollArea::vertical().show(ui, |ui| {
connection_state_ui(ui, &self.connection_state); connection_state_ui(ui, &self.connection_state);
summary_ui(ui, self.last_summary.as_ref(), &self.summary); summary_ui(ui, self.last_summary.as_ref(), &self.summary);
invoices_ui(ui, &self.invoices); invoices_ui(ui, &self.invoice_zap_reqs, ctx, &self.invoices);
channels_ui(ui, &self.channels); channels_ui(ui, &self.channels);
get_info_ui(ui, &self.get_info); get_info_ui(ui, &self.get_info);
}); });
@@ -251,7 +261,7 @@ impl ClnDash {
}); });
} }
fn process_events(&mut self) { fn process_events(&mut self, ndb: &Ndb) {
let Some(channel) = &mut self.channel else { let Some(channel) = &mut self.channel else {
return; return;
}; };
@@ -289,6 +299,23 @@ impl ClnDash {
} }
ClnResponse::PaidInvoices(invoices) => { ClnResponse::PaidInvoices(invoices) => {
// process zap requests
if let Ok(invoices) = &invoices {
for invoice in invoices {
let zap_req_id: Option<ZapReqId> =
serde_json::from_str(&invoice.description).ok();
if let Some(zap_req_id) = zap_req_id {
self.invoice_zap_reqs
.insert(invoice.label.clone(), zap_req_id.id);
let _ = ndb.process_event(&format!(
"[\"EVENT\",\"a\",{}]",
&invoice.description
));
}
}
}
self.invoices = LoadingState::from_result(invoices); self.invoices = LoadingState::from_result(invoices);
} }
}, },
@@ -600,7 +627,12 @@ fn delta_str(new: i64, old: i64) -> String {
} }
} }
fn invoices_ui(ui: &mut egui::Ui, invoices: &LoadingState<Vec<Invoice>, lnsocket::Error>) { fn invoices_ui(
ui: &mut egui::Ui,
invoice_notes: &HashMap<String, [u8; 32]>,
ctx: &mut AppContext,
invoices: &LoadingState<Vec<Invoice>, lnsocket::Error>,
) {
match invoices { match invoices {
LoadingState::Loading => { LoadingState::Loading => {
ui.label("loading invoices..."); ui.label("loading invoices...");
@@ -618,17 +650,23 @@ fn invoices_ui(ui: &mut egui::Ui, invoices: &LoadingState<Vec<Invoice>, lnsocket
.column(Column::remainder()) .column(Column::remainder())
.header(20.0, |mut header| { .header(20.0, |mut header| {
header.col(|ui| { header.col(|ui| {
ui.heading("Description"); ui.strong("description");
}); });
header.col(|ui| { header.col(|ui| {
ui.heading("Amount"); ui.strong("amount");
}); });
}) })
.body(|mut body| { .body(|mut body| {
for invoice in invoices { for invoice in invoices {
body.row(30.0, |mut row| { body.row(20.0, |mut row| {
row.col(|ui| { row.col(|ui| {
ui.label(invoice.description.clone()); if invoice.description.starts_with("{") {
ui.label("Zap!").on_hover_ui_at_pointer(|ui| {
note_hover_ui(ui, &invoice.label, ctx, invoice_notes);
});
} else {
ui.label(&invoice.description);
}
}); });
row.col(|ui| match invoice.bolt11.amount_milli_satoshis() { row.col(|ui| match invoice.bolt11.amount_milli_satoshis() {
None => { None => {
@@ -644,3 +682,71 @@ fn invoices_ui(ui: &mut egui::Ui, invoices: &LoadingState<Vec<Invoice>, lnsocket
} }
} }
} }
fn note_hover_ui(
ui: &mut egui::Ui,
label: &str,
ctx: &mut AppContext,
invoice_notes: &HashMap<String, [u8; 32]>,
) -> Option<notedeck::NoteAction> {
let zap_req_id = invoice_notes.get(label)?;
let Ok(txn) = nostrdb::Transaction::new(ctx.ndb) else {
return None;
};
let Ok(zapreq_note) = ctx.ndb.get_note_by_id(&txn, zap_req_id) else {
return None;
};
for tag in zapreq_note.tags() {
let Some("e") = tag.get_str(0) else {
continue;
};
let Some(target_id) = tag.get_id(1) else {
continue;
};
let Ok(note) = ctx.ndb.get_note_by_id(&txn, target_id) else {
return None;
};
let author = ctx
.ndb
.get_profile_by_pubkey(&txn, zapreq_note.pubkey())
.ok();
// TODO(jb55): make this less horrible
let mut note_context = notedeck::NoteContext {
ndb: ctx.ndb,
accounts: ctx.accounts,
img_cache: ctx.img_cache,
note_cache: ctx.note_cache,
zaps: ctx.zaps,
pool: ctx.pool,
job_pool: ctx.job_pool,
unknown_ids: ctx.unknown_ids,
clipboard: ctx.clipboard,
i18n: ctx.i18n,
global_wallet: ctx.global_wallet,
};
let mut jobs = notedeck::JobsCache::default();
let options = notedeck_ui::NoteOptions::default();
notedeck_ui::ProfilePic::from_profile_or_default(note_context.img_cache, author.as_ref())
.ui(ui);
let nostr_name = notedeck::name::get_display_name(author.as_ref());
ui.label(format!("{} zapped you", nostr_name.name()));
return notedeck_ui::NoteView::new(&mut note_context, &note, options, &mut jobs)
.preview_style()
.hide_media(true)
.show(ui)
.action;
}
None
}