124
crates/notedeck_clndash/src/channels.rs
Normal file
124
crates/notedeck_clndash/src/channels.rs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
use crate::event::LoadingState;
|
||||||
|
use crate::ui;
|
||||||
|
use egui::Color32;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct ListPeerChannel {
|
||||||
|
pub short_channel_id: String,
|
||||||
|
pub our_reserve_msat: i64,
|
||||||
|
pub to_us_msat: i64,
|
||||||
|
pub total_msat: i64,
|
||||||
|
pub their_reserve_msat: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Channel {
|
||||||
|
pub to_us: i64,
|
||||||
|
pub to_them: i64,
|
||||||
|
pub original: ListPeerChannel,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Channels {
|
||||||
|
pub max_total_msat: i64,
|
||||||
|
pub avail_in: i64,
|
||||||
|
pub avail_out: i64,
|
||||||
|
pub channels: Vec<Channel>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn channels_ui(ui: &mut egui::Ui, channels: &LoadingState<Channels, lnsocket::Error>) {
|
||||||
|
match channels {
|
||||||
|
LoadingState::Loaded(channels) => {
|
||||||
|
if channels.channels.is_empty() {
|
||||||
|
ui.label("no channels yet...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for channel in &channels.channels {
|
||||||
|
channel_ui(ui, channel, channels.max_total_msat);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.label(format!(
|
||||||
|
"available out {}",
|
||||||
|
ui::human_sat(channels.avail_out)
|
||||||
|
));
|
||||||
|
ui.label(format!("available in {}", ui::human_sat(channels.avail_in)));
|
||||||
|
}
|
||||||
|
LoadingState::Failed(err) => {
|
||||||
|
ui.label(format!("error fetching channels: {err}"));
|
||||||
|
}
|
||||||
|
LoadingState::Loading => {
|
||||||
|
ui.label("fetching channels...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn channel_ui(ui: &mut egui::Ui, c: &Channel, max_total_msat: i64) {
|
||||||
|
// ---------- numbers ----------
|
||||||
|
let short_channel_id = &c.original.short_channel_id;
|
||||||
|
|
||||||
|
let cap_ratio = (c.original.total_msat as f32 / max_total_msat.max(1) as f32).clamp(0.0, 1.0);
|
||||||
|
// Feel free to switch to log scaling if you have whales:
|
||||||
|
//let cap_ratio = ((c.original.total_msat as f32 + 1.0).log10() / (max_total_msat as f32 + 1.0).log10()).clamp(0.0, 1.0);
|
||||||
|
|
||||||
|
// ---------- colors & style ----------
|
||||||
|
let out_color = Color32::from_rgb(84, 69, 201); // blue
|
||||||
|
let in_color = Color32::from_rgb(158, 56, 180); // purple
|
||||||
|
|
||||||
|
// Thickness scales with capacity, but keeps a nice minimum
|
||||||
|
let thickness = 10.0 + cap_ratio * 22.0; // 10 → 32 px
|
||||||
|
let row_h = thickness + 14.0;
|
||||||
|
|
||||||
|
// ---------- layout ----------
|
||||||
|
let (rect, response) = ui.allocate_exact_size(
|
||||||
|
egui::vec2(ui.available_width(), row_h),
|
||||||
|
egui::Sense::hover(),
|
||||||
|
);
|
||||||
|
let painter = ui.painter_at(rect);
|
||||||
|
|
||||||
|
let bar_rect = egui::Rect::from_min_max(
|
||||||
|
egui::pos2(rect.left(), rect.center().y - thickness * 0.5),
|
||||||
|
egui::pos2(rect.right(), rect.center().y + thickness * 0.5),
|
||||||
|
);
|
||||||
|
let corner_radius = (thickness * 0.5) as u8;
|
||||||
|
let out_radius = egui::CornerRadius {
|
||||||
|
ne: 0,
|
||||||
|
nw: corner_radius,
|
||||||
|
sw: corner_radius,
|
||||||
|
se: 0,
|
||||||
|
};
|
||||||
|
let in_radius = egui::CornerRadius {
|
||||||
|
ne: corner_radius,
|
||||||
|
nw: 0,
|
||||||
|
sw: 0,
|
||||||
|
se: corner_radius,
|
||||||
|
};
|
||||||
|
/*
|
||||||
|
painter.rect_filled(bar_rect, rounding, track_color);
|
||||||
|
painter.rect_stroke(bar_rect, rounding, track_stroke, egui::StrokeKind::Middle);
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Split widths
|
||||||
|
let usable = (c.to_us + c.to_them).max(1) as f32;
|
||||||
|
let out_w = (bar_rect.width() * (c.to_us as f32 / usable)).round();
|
||||||
|
let split_x = bar_rect.left() + out_w;
|
||||||
|
|
||||||
|
// Outbound fill (left)
|
||||||
|
let out_rect = egui::Rect::from_min_max(bar_rect.min, egui::pos2(split_x, bar_rect.max.y));
|
||||||
|
if out_rect.width() > 0.5 {
|
||||||
|
painter.rect_filled(out_rect, out_radius, out_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inbound fill (right)
|
||||||
|
let in_rect = egui::Rect::from_min_max(egui::pos2(split_x, bar_rect.min.y), bar_rect.max);
|
||||||
|
if in_rect.width() > 0.5 {
|
||||||
|
painter.rect_filled(in_rect, in_radius, in_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tooltip
|
||||||
|
response.on_hover_text_at_pointer(format!(
|
||||||
|
"Channel ID {short_channel_id}\nOutbound (ours): {} sats\nInbound (theirs): {} sats\nCapacity: {} sats",
|
||||||
|
ui::human_sat(c.to_us),
|
||||||
|
ui::human_sat(c.to_them),
|
||||||
|
ui::human_sat(c.original.total_msat),
|
||||||
|
));
|
||||||
|
}
|
||||||
@@ -1,7 +1,52 @@
|
|||||||
use lightning_invoice::Bolt11Invoice;
|
use crate::channels::Channels;
|
||||||
use serde::{Deserialize, Serialize};
|
use crate::invoice::Invoice;
|
||||||
|
use serde::Serialize;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
|
pub enum ConnectionState {
|
||||||
|
Dead(String),
|
||||||
|
Connecting,
|
||||||
|
Active,
|
||||||
|
}
|
||||||
|
pub enum LoadingState<T, E> {
|
||||||
|
Loading,
|
||||||
|
Failed(E),
|
||||||
|
Loaded(T),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, E> Default for LoadingState<T, E> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Loading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, E> LoadingState<T, E> {
|
||||||
|
fn _as_ref(&self) -> LoadingState<&T, &E> {
|
||||||
|
match self {
|
||||||
|
Self::Loading => LoadingState::<&T, &E>::Loading,
|
||||||
|
Self::Failed(err) => LoadingState::<&T, &E>::Failed(err),
|
||||||
|
Self::Loaded(t) => LoadingState::<&T, &E>::Loaded(t),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_result(res: Result<T, E>) -> LoadingState<T, E> {
|
||||||
|
match res {
|
||||||
|
Ok(r) => LoadingState::Loaded(r),
|
||||||
|
Err(err) => LoadingState::Failed(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
fn unwrap(self) -> T {
|
||||||
|
let Self::Loaded(t) = self else {
|
||||||
|
panic!("unwrap in LoadingState");
|
||||||
|
};
|
||||||
|
|
||||||
|
t
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Debug, Clone)]
|
#[derive(Serialize, Debug, Clone)]
|
||||||
pub struct WaitRequest {
|
pub struct WaitRequest {
|
||||||
pub indexname: String,
|
pub indexname: String,
|
||||||
@@ -16,42 +61,6 @@ pub enum Request {
|
|||||||
PaidInvoices(u32),
|
PaidInvoices(u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
pub struct ListPeerChannel {
|
|
||||||
pub short_channel_id: String,
|
|
||||||
pub our_reserve_msat: i64,
|
|
||||||
pub to_us_msat: i64,
|
|
||||||
pub total_msat: i64,
|
|
||||||
pub their_reserve_msat: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Channel {
|
|
||||||
pub to_us: i64,
|
|
||||||
pub to_them: i64,
|
|
||||||
pub original: ListPeerChannel,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Channels {
|
|
||||||
pub max_total_msat: i64,
|
|
||||||
pub avail_in: i64,
|
|
||||||
pub avail_out: i64,
|
|
||||||
pub channels: Vec<Channel>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Invoice {
|
|
||||||
pub lastpay_index: Option<u64>,
|
|
||||||
pub label: String,
|
|
||||||
pub bolt11: Bolt11Invoice,
|
|
||||||
pub payment_hash: String,
|
|
||||||
pub amount_msat: u64,
|
|
||||||
pub status: String,
|
|
||||||
pub description: String,
|
|
||||||
pub expires_at: u64,
|
|
||||||
pub created_index: u64,
|
|
||||||
pub updated_index: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Responses from the socket
|
/// Responses from the socket
|
||||||
pub enum ClnResponse {
|
pub enum ClnResponse {
|
||||||
GetInfo(Value),
|
GetInfo(Value),
|
||||||
|
|||||||
77
crates/notedeck_clndash/src/invoice.rs
Normal file
77
crates/notedeck_clndash/src/invoice.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
use crate::event::LoadingState;
|
||||||
|
use crate::ui;
|
||||||
|
use lightning_invoice::Bolt11Invoice;
|
||||||
|
use notedeck::AppContext;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct Invoice {
|
||||||
|
pub lastpay_index: Option<u64>,
|
||||||
|
pub label: String,
|
||||||
|
pub bolt11: Bolt11Invoice,
|
||||||
|
pub payment_hash: String,
|
||||||
|
pub amount_msat: u64,
|
||||||
|
pub status: String,
|
||||||
|
pub description: String,
|
||||||
|
pub expires_at: u64,
|
||||||
|
pub created_index: u64,
|
||||||
|
pub updated_index: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invoices_ui(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
invoice_notes: &HashMap<String, [u8; 32]>,
|
||||||
|
ctx: &mut AppContext,
|
||||||
|
invoices: &LoadingState<Vec<Invoice>, lnsocket::Error>,
|
||||||
|
) {
|
||||||
|
match invoices {
|
||||||
|
LoadingState::Loading => {
|
||||||
|
ui.label("loading invoices...");
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadingState::Failed(err) => {
|
||||||
|
ui.label(format!("failed to load invoices: {err}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadingState::Loaded(invoices) => {
|
||||||
|
use egui_extras::{Column, TableBuilder};
|
||||||
|
|
||||||
|
TableBuilder::new(ui)
|
||||||
|
.column(Column::auto().resizable(true))
|
||||||
|
.column(Column::remainder())
|
||||||
|
.vscroll(false)
|
||||||
|
.header(20.0, |mut header| {
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.strong("description");
|
||||||
|
});
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.strong("amount");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.body(|mut body| {
|
||||||
|
for invoice in invoices {
|
||||||
|
body.row(20.0, |mut row| {
|
||||||
|
row.col(|ui| {
|
||||||
|
if invoice.description.starts_with("{") {
|
||||||
|
ui.label("Zap!").on_hover_ui_at_pointer(|ui| {
|
||||||
|
ui::note_hover_ui(ui, &invoice.label, ctx, invoice_notes);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ui.label(&invoice.description);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
row.col(|ui| match invoice.bolt11.amount_milli_satoshis() {
|
||||||
|
None => {
|
||||||
|
ui.label("any");
|
||||||
|
}
|
||||||
|
Some(amt) => {
|
||||||
|
ui.label(ui::human_verbose_sat(amt as i64));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
use crate::event::Channel;
|
use crate::channels::Channel;
|
||||||
use crate::event::Channels;
|
use crate::channels::Channels;
|
||||||
|
use crate::channels::ListPeerChannel;
|
||||||
use crate::event::ClnResponse;
|
use crate::event::ClnResponse;
|
||||||
|
use crate::event::ConnectionState;
|
||||||
use crate::event::Event;
|
use crate::event::Event;
|
||||||
use crate::event::Invoice;
|
use crate::event::LoadingState;
|
||||||
use crate::event::ListPeerChannel;
|
|
||||||
use crate::event::Request;
|
use crate::event::Request;
|
||||||
|
use crate::invoice::Invoice;
|
||||||
|
use crate::summary::Summary;
|
||||||
use crate::watch::fetch_paid_invoices;
|
use crate::watch::fetch_paid_invoices;
|
||||||
|
|
||||||
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 nostrdb::Ndb;
|
||||||
@@ -18,48 +20,13 @@ 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};
|
||||||
|
|
||||||
|
mod channels;
|
||||||
mod event;
|
mod event;
|
||||||
|
mod invoice;
|
||||||
|
mod summary;
|
||||||
|
mod ui;
|
||||||
mod watch;
|
mod watch;
|
||||||
|
|
||||||
pub enum LoadingState<T, E> {
|
|
||||||
Loading,
|
|
||||||
Failed(E),
|
|
||||||
Loaded(T),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, E> Default for LoadingState<T, E> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Loading
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, E> LoadingState<T, E> {
|
|
||||||
fn _as_ref(&self) -> LoadingState<&T, &E> {
|
|
||||||
match self {
|
|
||||||
Self::Loading => LoadingState::<&T, &E>::Loading,
|
|
||||||
Self::Failed(err) => LoadingState::<&T, &E>::Failed(err),
|
|
||||||
Self::Loaded(t) => LoadingState::<&T, &E>::Loaded(t),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_result(res: Result<T, E>) -> LoadingState<T, E> {
|
|
||||||
match res {
|
|
||||||
Ok(r) => LoadingState::Loaded(r),
|
|
||||||
Err(err) => LoadingState::Failed(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
fn unwrap(self) -> T {
|
|
||||||
let Self::Loaded(t) = self else {
|
|
||||||
panic!("unwrap in LoadingState");
|
|
||||||
};
|
|
||||||
|
|
||||||
t
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ClnDash {
|
pub struct ClnDash {
|
||||||
initialized: bool,
|
initialized: bool,
|
||||||
@@ -91,12 +58,6 @@ struct CommChannel {
|
|||||||
event_rx: UnboundedReceiver<Event>,
|
event_rx: UnboundedReceiver<Event>,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ConnectionState {
|
|
||||||
Dead(String),
|
|
||||||
Connecting,
|
|
||||||
Active,
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
||||||
@@ -114,56 +75,17 @@ impl notedeck::App for ClnDash {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn connection_state_ui(ui: &mut egui::Ui, state: &ConnectionState) {
|
|
||||||
match state {
|
|
||||||
ConnectionState::Active => {
|
|
||||||
ui.add(Label::new(RichText::new("Connected").color(Color32::GREEN)));
|
|
||||||
}
|
|
||||||
|
|
||||||
ConnectionState::Connecting => {
|
|
||||||
ui.add(Label::new(
|
|
||||||
RichText::new("Connecting").color(Color32::YELLOW),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
ConnectionState::Dead(reason) => {
|
|
||||||
ui.add(Label::new(
|
|
||||||
RichText::new(format!("Disconnected: {reason}")).color(Color32::RED),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn summary_ui(
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
last_summary: Option<&Summary>,
|
|
||||||
summary: &LoadingState<Summary, lnsocket::Error>,
|
|
||||||
) {
|
|
||||||
match summary {
|
|
||||||
LoadingState::Loading => {
|
|
||||||
ui.label("loading summary");
|
|
||||||
}
|
|
||||||
LoadingState::Failed(err) => {
|
|
||||||
ui.label(format!("Failed to get summary: {err}"));
|
|
||||||
}
|
|
||||||
LoadingState::Loaded(summary) => {
|
|
||||||
summary_cards_ui(ui, summary, last_summary);
|
|
||||||
ui.add_space(8.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClnDash {
|
impl ClnDash {
|
||||||
fn show(&mut self, ui: &mut egui::Ui, ctx: &mut AppContext) {
|
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);
|
ui::connection_state_ui(ui, &self.connection_state);
|
||||||
summary_ui(ui, self.last_summary.as_ref(), &self.summary);
|
crate::summary::summary_ui(ui, self.last_summary.as_ref(), &self.summary);
|
||||||
invoices_ui(ui, &self.invoice_zap_reqs, ctx, &self.invoices);
|
crate::invoice::invoices_ui(ui, &self.invoice_zap_reqs, ctx, &self.invoices);
|
||||||
channels_ui(ui, &self.channels);
|
crate::channels::channels_ui(ui, &self.channels);
|
||||||
get_info_ui(ui, &self.get_info);
|
crate::ui::get_info_ui(ui, &self.get_info);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -282,11 +204,13 @@ impl ClnDash {
|
|||||||
Event::Response(resp) => match resp {
|
Event::Response(resp) => match resp {
|
||||||
ClnResponse::ListPeerChannels(chans) => {
|
ClnResponse::ListPeerChannels(chans) => {
|
||||||
if let LoadingState::Loaded(prev) = &self.channels {
|
if let LoadingState::Loaded(prev) = &self.channels {
|
||||||
self.last_summary = Some(compute_summary(prev));
|
self.last_summary = Some(crate::summary::compute_summary(prev));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.summary = match &chans {
|
self.summary = match &chans {
|
||||||
Ok(chans) => LoadingState::Loaded(compute_summary(chans)),
|
Ok(chans) => {
|
||||||
|
LoadingState::Loaded(crate::summary::compute_summary(chans))
|
||||||
|
}
|
||||||
Err(err) => LoadingState::Failed(err.clone()),
|
Err(err) => LoadingState::Failed(err.clone()),
|
||||||
};
|
};
|
||||||
self.channels = LoadingState::from_result(chans);
|
self.channels = LoadingState::from_result(chans);
|
||||||
@@ -324,141 +248,6 @@ impl ClnDash {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_info_ui(ui: &mut egui::Ui, info: &LoadingState<String, lnsocket::Error>) {
|
|
||||||
ui.horizontal_wrapped(|ui| match info {
|
|
||||||
LoadingState::Loading => {}
|
|
||||||
LoadingState::Failed(err) => {
|
|
||||||
ui.label(format!("failed to fetch node info: {err}"));
|
|
||||||
}
|
|
||||||
LoadingState::Loaded(info) => {
|
|
||||||
ui.add(Label::new(info).wrap_mode(egui::TextWrapMode::Wrap));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn channel_ui(ui: &mut egui::Ui, c: &Channel, max_total_msat: i64) {
|
|
||||||
// ---------- numbers ----------
|
|
||||||
let short_channel_id = &c.original.short_channel_id;
|
|
||||||
|
|
||||||
let cap_ratio = (c.original.total_msat as f32 / max_total_msat.max(1) as f32).clamp(0.0, 1.0);
|
|
||||||
// Feel free to switch to log scaling if you have whales:
|
|
||||||
//let cap_ratio = ((c.original.total_msat as f32 + 1.0).log10() / (max_total_msat as f32 + 1.0).log10()).clamp(0.0, 1.0);
|
|
||||||
|
|
||||||
// ---------- colors & style ----------
|
|
||||||
let out_color = Color32::from_rgb(84, 69, 201); // blue
|
|
||||||
let in_color = Color32::from_rgb(158, 56, 180); // purple
|
|
||||||
|
|
||||||
// Thickness scales with capacity, but keeps a nice minimum
|
|
||||||
let thickness = 10.0 + cap_ratio * 22.0; // 10 → 32 px
|
|
||||||
let row_h = thickness + 14.0;
|
|
||||||
|
|
||||||
// ---------- layout ----------
|
|
||||||
let (rect, response) = ui.allocate_exact_size(
|
|
||||||
egui::vec2(ui.available_width(), row_h),
|
|
||||||
egui::Sense::hover(),
|
|
||||||
);
|
|
||||||
let painter = ui.painter_at(rect);
|
|
||||||
|
|
||||||
let bar_rect = egui::Rect::from_min_max(
|
|
||||||
egui::pos2(rect.left(), rect.center().y - thickness * 0.5),
|
|
||||||
egui::pos2(rect.right(), rect.center().y + thickness * 0.5),
|
|
||||||
);
|
|
||||||
let corner_radius = (thickness * 0.5) as u8;
|
|
||||||
let out_radius = egui::CornerRadius {
|
|
||||||
ne: 0,
|
|
||||||
nw: corner_radius,
|
|
||||||
sw: corner_radius,
|
|
||||||
se: 0,
|
|
||||||
};
|
|
||||||
let in_radius = egui::CornerRadius {
|
|
||||||
ne: corner_radius,
|
|
||||||
nw: 0,
|
|
||||||
sw: 0,
|
|
||||||
se: corner_radius,
|
|
||||||
};
|
|
||||||
/*
|
|
||||||
painter.rect_filled(bar_rect, rounding, track_color);
|
|
||||||
painter.rect_stroke(bar_rect, rounding, track_stroke, egui::StrokeKind::Middle);
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Split widths
|
|
||||||
let usable = (c.to_us + c.to_them).max(1) as f32;
|
|
||||||
let out_w = (bar_rect.width() * (c.to_us as f32 / usable)).round();
|
|
||||||
let split_x = bar_rect.left() + out_w;
|
|
||||||
|
|
||||||
// Outbound fill (left)
|
|
||||||
let out_rect = egui::Rect::from_min_max(bar_rect.min, egui::pos2(split_x, bar_rect.max.y));
|
|
||||||
if out_rect.width() > 0.5 {
|
|
||||||
painter.rect_filled(out_rect, out_radius, out_color);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inbound fill (right)
|
|
||||||
let in_rect = egui::Rect::from_min_max(egui::pos2(split_x, bar_rect.min.y), bar_rect.max);
|
|
||||||
if in_rect.width() > 0.5 {
|
|
||||||
painter.rect_filled(in_rect, in_radius, in_color);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tooltip
|
|
||||||
response.on_hover_text_at_pointer(format!(
|
|
||||||
"Channel ID {short_channel_id}\nOutbound (ours): {} sats\nInbound (theirs): {} sats\nCapacity: {} sats",
|
|
||||||
human_sat(c.to_us),
|
|
||||||
human_sat(c.to_them),
|
|
||||||
human_sat(c.original.total_msat),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- helper ----------
|
|
||||||
fn human_sat(msat: i64) -> String {
|
|
||||||
let sats = msat / 1000;
|
|
||||||
if sats >= 1_000_000 {
|
|
||||||
format!("{:.1}M", sats as f64 / 1_000_000.0)
|
|
||||||
} else if sats >= 1_000 {
|
|
||||||
format!("{:.1}k", sats as f64 / 1_000.0)
|
|
||||||
} else {
|
|
||||||
sats.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn human_verbose_sat(msat: i64) -> String {
|
|
||||||
if msat < 1_000 {
|
|
||||||
// less than 1 sat
|
|
||||||
format!("{msat} msat")
|
|
||||||
} else {
|
|
||||||
let sats = msat / 1_000;
|
|
||||||
if sats < 100_000_000 {
|
|
||||||
// less than 1 BTC
|
|
||||||
format!("{sats} sat")
|
|
||||||
} else {
|
|
||||||
let btc = sats / 100_000_000;
|
|
||||||
format!("{btc} BTC")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn channels_ui(ui: &mut egui::Ui, channels: &LoadingState<Channels, lnsocket::Error>) {
|
|
||||||
match channels {
|
|
||||||
LoadingState::Loaded(channels) => {
|
|
||||||
if channels.channels.is_empty() {
|
|
||||||
ui.label("no channels yet...");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for channel in &channels.channels {
|
|
||||||
channel_ui(ui, channel, channels.max_total_msat);
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.label(format!("available out {}", human_sat(channels.avail_out)));
|
|
||||||
ui.label(format!("available in {}", human_sat(channels.avail_in)));
|
|
||||||
}
|
|
||||||
LoadingState::Failed(err) => {
|
|
||||||
ui.label(format!("error fetching channels: {err}"));
|
|
||||||
}
|
|
||||||
LoadingState::Loading => {
|
|
||||||
ui.label("fetching channels...");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_channels(peer_channels: Vec<ListPeerChannel>) -> Channels {
|
fn to_channels(peer_channels: Vec<ListPeerChannel>) -> Channels {
|
||||||
let mut avail_out: i64 = 0;
|
let mut avail_out: i64 = 0;
|
||||||
let mut avail_in: i64 = 0;
|
let mut avail_in: i64 = 0;
|
||||||
@@ -499,255 +288,3 @@ fn to_channels(peer_channels: Vec<ListPeerChannel>) -> Channels {
|
|||||||
channels,
|
channels,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn summary_cards_ui(ui: &mut egui::Ui, s: &Summary, prev: Option<&Summary>) {
|
|
||||||
let old = prev.cloned().unwrap_or_default();
|
|
||||||
let items: [(&str, String, Option<String>); 6] = [
|
|
||||||
(
|
|
||||||
"Total capacity",
|
|
||||||
human_sat(s.total_msat),
|
|
||||||
prev.map(|_| delta_str(s.total_msat, old.total_msat)),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"Avail out",
|
|
||||||
human_sat(s.avail_out_msat),
|
|
||||||
prev.map(|_| delta_str(s.avail_out_msat, old.avail_out_msat)),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"Avail in",
|
|
||||||
human_sat(s.avail_in_msat),
|
|
||||||
prev.map(|_| delta_str(s.avail_in_msat, old.avail_in_msat)),
|
|
||||||
),
|
|
||||||
("# Channels", s.channel_count.to_string(), None),
|
|
||||||
("Largest", human_sat(s.largest_msat), None),
|
|
||||||
(
|
|
||||||
"Outbound %",
|
|
||||||
format!("{:.0}%", s.outbound_pct * 100.0),
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
// --- responsive columns ---
|
|
||||||
let min_card = 160.0;
|
|
||||||
let cols = ((ui.available_width() / min_card).floor() as usize).max(1);
|
|
||||||
|
|
||||||
egui::Grid::new("summary_grid")
|
|
||||||
.num_columns(cols)
|
|
||||||
.min_col_width(min_card)
|
|
||||||
.spacing(egui::vec2(8.0, 8.0))
|
|
||||||
.show(ui, |ui| {
|
|
||||||
let items_len = items.len();
|
|
||||||
for (i, (t, v, d)) in items.into_iter().enumerate() {
|
|
||||||
card_cell(ui, t, v, d, min_card);
|
|
||||||
|
|
||||||
// End the row when we filled a row worth of cells
|
|
||||||
if (i + 1) % cols == 0 {
|
|
||||||
ui.end_row();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the last row wasn't full, close it anyway
|
|
||||||
if items_len % cols != 0 {
|
|
||||||
ui.end_row();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn card_cell(ui: &mut egui::Ui, title: &str, value: String, delta: Option<String>, min_card: f32) {
|
|
||||||
let weak = ui.visuals().weak_text_color();
|
|
||||||
egui::Frame::group(ui.style())
|
|
||||||
.fill(ui.visuals().extreme_bg_color)
|
|
||||||
.corner_radius(egui::CornerRadius::same(10))
|
|
||||||
.inner_margin(egui::Margin::same(10))
|
|
||||||
.stroke(ui.visuals().widgets.noninteractive.bg_stroke)
|
|
||||||
.show(ui, |ui| {
|
|
||||||
ui.set_min_width(min_card);
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.add(
|
|
||||||
egui::Label::new(egui::RichText::new(title).small().color(weak))
|
|
||||||
.wrap_mode(egui::TextWrapMode::Wrap),
|
|
||||||
);
|
|
||||||
ui.add_space(4.0);
|
|
||||||
ui.add(
|
|
||||||
egui::Label::new(egui::RichText::new(value).strong().size(18.0))
|
|
||||||
.wrap_mode(egui::TextWrapMode::Wrap),
|
|
||||||
);
|
|
||||||
if let Some(d) = delta {
|
|
||||||
ui.add_space(2.0);
|
|
||||||
ui.add(
|
|
||||||
egui::Label::new(egui::RichText::new(d).small().color(weak))
|
|
||||||
.wrap_mode(egui::TextWrapMode::Wrap),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ui.set_min_height(20.0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
|
||||||
struct Summary {
|
|
||||||
total_msat: i64,
|
|
||||||
avail_out_msat: i64,
|
|
||||||
avail_in_msat: i64,
|
|
||||||
channel_count: usize,
|
|
||||||
largest_msat: i64,
|
|
||||||
outbound_pct: f32, // fraction of total capacity
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_summary(ch: &Channels) -> Summary {
|
|
||||||
let total_msat: i64 = ch.channels.iter().map(|c| c.original.total_msat).sum();
|
|
||||||
let largest_msat: i64 = ch
|
|
||||||
.channels
|
|
||||||
.iter()
|
|
||||||
.map(|c| c.original.total_msat)
|
|
||||||
.max()
|
|
||||||
.unwrap_or(0);
|
|
||||||
let outbound_pct = if total_msat > 0 {
|
|
||||||
ch.avail_out as f32 / total_msat as f32
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
|
|
||||||
Summary {
|
|
||||||
total_msat,
|
|
||||||
avail_out_msat: ch.avail_out,
|
|
||||||
avail_in_msat: ch.avail_in,
|
|
||||||
channel_count: ch.channels.len(),
|
|
||||||
largest_msat,
|
|
||||||
outbound_pct,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delta_str(new: i64, old: i64) -> String {
|
|
||||||
let d = new - old;
|
|
||||||
match d.cmp(&0) {
|
|
||||||
std::cmp::Ordering::Greater => format!("↑ {}", human_sat(d)),
|
|
||||||
std::cmp::Ordering::Less => format!("↓ {}", human_sat(-d)),
|
|
||||||
std::cmp::Ordering::Equal => "·".into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn invoices_ui(
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
invoice_notes: &HashMap<String, [u8; 32]>,
|
|
||||||
ctx: &mut AppContext,
|
|
||||||
invoices: &LoadingState<Vec<Invoice>, lnsocket::Error>,
|
|
||||||
) {
|
|
||||||
match invoices {
|
|
||||||
LoadingState::Loading => {
|
|
||||||
ui.label("loading invoices...");
|
|
||||||
}
|
|
||||||
|
|
||||||
LoadingState::Failed(err) => {
|
|
||||||
ui.label(format!("failed to load invoices: {err}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
LoadingState::Loaded(invoices) => {
|
|
||||||
use egui_extras::{Column, TableBuilder};
|
|
||||||
|
|
||||||
TableBuilder::new(ui)
|
|
||||||
.column(Column::auto().resizable(true))
|
|
||||||
.column(Column::remainder())
|
|
||||||
.vscroll(false)
|
|
||||||
.header(20.0, |mut header| {
|
|
||||||
header.col(|ui| {
|
|
||||||
ui.strong("description");
|
|
||||||
});
|
|
||||||
header.col(|ui| {
|
|
||||||
ui.strong("amount");
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.body(|mut body| {
|
|
||||||
for invoice in invoices {
|
|
||||||
body.row(20.0, |mut row| {
|
|
||||||
row.col(|ui| {
|
|
||||||
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() {
|
|
||||||
None => {
|
|
||||||
ui.label("any");
|
|
||||||
}
|
|
||||||
Some(amt) => {
|
|
||||||
ui.label(human_verbose_sat(amt as i64));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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, ¬e, options, &mut jobs)
|
|
||||||
.preview_style()
|
|
||||||
.hide_media(true)
|
|
||||||
.show(ui)
|
|
||||||
.action;
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|||||||
140
crates/notedeck_clndash/src/summary.rs
Normal file
140
crates/notedeck_clndash/src/summary.rs
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
use crate::channels::Channels;
|
||||||
|
use crate::event::LoadingState;
|
||||||
|
use crate::ui;
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct Summary {
|
||||||
|
pub total_msat: i64,
|
||||||
|
pub avail_out_msat: i64,
|
||||||
|
pub avail_in_msat: i64,
|
||||||
|
pub channel_count: usize,
|
||||||
|
pub largest_msat: i64,
|
||||||
|
pub outbound_pct: f32, // fraction of total capacity
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compute_summary(ch: &Channels) -> Summary {
|
||||||
|
let total_msat: i64 = ch.channels.iter().map(|c| c.original.total_msat).sum();
|
||||||
|
let largest_msat: i64 = ch
|
||||||
|
.channels
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.original.total_msat)
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0);
|
||||||
|
let outbound_pct = if total_msat > 0 {
|
||||||
|
ch.avail_out as f32 / total_msat as f32
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
Summary {
|
||||||
|
total_msat,
|
||||||
|
avail_out_msat: ch.avail_out,
|
||||||
|
avail_in_msat: ch.avail_in,
|
||||||
|
channel_count: ch.channels.len(),
|
||||||
|
largest_msat,
|
||||||
|
outbound_pct,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn summary_ui(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
last_summary: Option<&Summary>,
|
||||||
|
summary: &LoadingState<Summary, lnsocket::Error>,
|
||||||
|
) {
|
||||||
|
match summary {
|
||||||
|
LoadingState::Loading => {
|
||||||
|
ui.label("loading summary");
|
||||||
|
}
|
||||||
|
LoadingState::Failed(err) => {
|
||||||
|
ui.label(format!("Failed to get summary: {err}"));
|
||||||
|
}
|
||||||
|
LoadingState::Loaded(summary) => {
|
||||||
|
summary_cards_ui(ui, summary, last_summary);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn summary_cards_ui(ui: &mut egui::Ui, s: &Summary, prev: Option<&Summary>) {
|
||||||
|
let old = prev.cloned().unwrap_or_default();
|
||||||
|
let items: [(&str, String, Option<String>); 6] = [
|
||||||
|
(
|
||||||
|
"Total capacity",
|
||||||
|
ui::human_sat(s.total_msat),
|
||||||
|
prev.map(|_| ui::delta_str(s.total_msat, old.total_msat)),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Avail out",
|
||||||
|
ui::human_sat(s.avail_out_msat),
|
||||||
|
prev.map(|_| ui::delta_str(s.avail_out_msat, old.avail_out_msat)),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Avail in",
|
||||||
|
ui::human_sat(s.avail_in_msat),
|
||||||
|
prev.map(|_| ui::delta_str(s.avail_in_msat, old.avail_in_msat)),
|
||||||
|
),
|
||||||
|
("# Channels", s.channel_count.to_string(), None),
|
||||||
|
("Largest", ui::human_sat(s.largest_msat), None),
|
||||||
|
(
|
||||||
|
"Outbound %",
|
||||||
|
format!("{:.0}%", s.outbound_pct * 100.0),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- responsive columns ---
|
||||||
|
let min_card = 160.0;
|
||||||
|
let cols = ((ui.available_width() / min_card).floor() as usize).max(1);
|
||||||
|
|
||||||
|
egui::Grid::new("summary_grid")
|
||||||
|
.num_columns(cols)
|
||||||
|
.min_col_width(min_card)
|
||||||
|
.spacing(egui::vec2(8.0, 8.0))
|
||||||
|
.show(ui, |ui| {
|
||||||
|
let items_len = items.len();
|
||||||
|
for (i, (t, v, d)) in items.into_iter().enumerate() {
|
||||||
|
card_cell(ui, t, v, d, min_card);
|
||||||
|
|
||||||
|
// End the row when we filled a row worth of cells
|
||||||
|
if (i + 1) % cols == 0 {
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the last row wasn't full, close it anyway
|
||||||
|
if items_len % cols != 0 {
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn card_cell(ui: &mut egui::Ui, title: &str, value: String, delta: Option<String>, min_card: f32) {
|
||||||
|
let weak = ui.visuals().weak_text_color();
|
||||||
|
egui::Frame::group(ui.style())
|
||||||
|
.fill(ui.visuals().extreme_bg_color)
|
||||||
|
.corner_radius(egui::CornerRadius::same(10))
|
||||||
|
.inner_margin(egui::Margin::same(10))
|
||||||
|
.stroke(ui.visuals().widgets.noninteractive.bg_stroke)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.set_min_width(min_card);
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.add(
|
||||||
|
egui::Label::new(egui::RichText::new(title).small().color(weak))
|
||||||
|
.wrap_mode(egui::TextWrapMode::Wrap),
|
||||||
|
);
|
||||||
|
ui.add_space(4.0);
|
||||||
|
ui.add(
|
||||||
|
egui::Label::new(egui::RichText::new(value).strong().size(18.0))
|
||||||
|
.wrap_mode(egui::TextWrapMode::Wrap),
|
||||||
|
);
|
||||||
|
if let Some(d) = delta {
|
||||||
|
ui.add_space(2.0);
|
||||||
|
ui.add(
|
||||||
|
egui::Label::new(egui::RichText::new(d).small().color(weak))
|
||||||
|
.wrap_mode(egui::TextWrapMode::Wrap),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.set_min_height(20.0);
|
||||||
|
});
|
||||||
|
}
|
||||||
145
crates/notedeck_clndash/src/ui.rs
Normal file
145
crates/notedeck_clndash/src/ui.rs
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
use crate::event::ConnectionState;
|
||||||
|
use crate::event::LoadingState;
|
||||||
|
use egui::Color32;
|
||||||
|
use egui::Label;
|
||||||
|
use egui::RichText;
|
||||||
|
use egui::Widget;
|
||||||
|
use notedeck::AppContext;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub 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, ¬e, options, &mut jobs)
|
||||||
|
.preview_style()
|
||||||
|
.hide_media(true)
|
||||||
|
.show(ui)
|
||||||
|
.action;
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_info_ui(ui: &mut egui::Ui, info: &LoadingState<String, lnsocket::Error>) {
|
||||||
|
ui.horizontal_wrapped(|ui| match info {
|
||||||
|
LoadingState::Loading => {}
|
||||||
|
LoadingState::Failed(err) => {
|
||||||
|
ui.label(format!("failed to fetch node info: {err}"));
|
||||||
|
}
|
||||||
|
LoadingState::Loaded(info) => {
|
||||||
|
ui.add(Label::new(info).wrap_mode(egui::TextWrapMode::Wrap));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn connection_state_ui(ui: &mut egui::Ui, state: &ConnectionState) {
|
||||||
|
match state {
|
||||||
|
ConnectionState::Active => {
|
||||||
|
ui.add(Label::new(RichText::new("Connected").color(Color32::GREEN)));
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectionState::Connecting => {
|
||||||
|
ui.add(Label::new(
|
||||||
|
RichText::new("Connecting").color(Color32::YELLOW),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectionState::Dead(reason) => {
|
||||||
|
ui.add(Label::new(
|
||||||
|
RichText::new(format!("Disconnected: {reason}")).color(Color32::RED),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- helper ----------
|
||||||
|
pub fn human_sat(msat: i64) -> String {
|
||||||
|
let sats = msat / 1000;
|
||||||
|
if sats >= 1_000_000 {
|
||||||
|
format!("{:.1}M", sats as f64 / 1_000_000.0)
|
||||||
|
} else if sats >= 1_000 {
|
||||||
|
format!("{:.1}k", sats as f64 / 1_000.0)
|
||||||
|
} else {
|
||||||
|
sats.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn human_verbose_sat(msat: i64) -> String {
|
||||||
|
if msat < 1_000 {
|
||||||
|
// less than 1 sat
|
||||||
|
format!("{msat} msat")
|
||||||
|
} else {
|
||||||
|
let sats = msat / 1_000;
|
||||||
|
if sats < 100_000_000 {
|
||||||
|
// less than 1 BTC
|
||||||
|
format!("{sats} sat")
|
||||||
|
} else {
|
||||||
|
let btc = sats / 100_000_000;
|
||||||
|
format!("{btc} BTC")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delta_str(new: i64, old: i64) -> String {
|
||||||
|
let d = new - old;
|
||||||
|
match d.cmp(&0) {
|
||||||
|
std::cmp::Ordering::Greater => format!("↑ {}", human_sat(d)),
|
||||||
|
std::cmp::Ordering::Less => format!("↓ {}", human_sat(-d)),
|
||||||
|
std::cmp::Ordering::Equal => "·".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::event::Invoice;
|
use crate::invoice::Invoice;
|
||||||
use lnsocket::CallOpts;
|
use lnsocket::CallOpts;
|
||||||
use lnsocket::CommandoClient;
|
use lnsocket::CommandoClient;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|||||||
Reference in New Issue
Block a user