commit cfbd601196a5657c10a89cb4b183b1155d823236
parent 5917bc16fd0c03c6f73c0fabfea46e4304834d7b
Author: kernelkind <kernelkind@gmail.com>
Date: Sun, 30 Mar 2025 15:11:52 -0400
note zap button
Signed-off-by: kernelkind <kernelkind@gmail.com>
Diffstat:
6 files changed, 175 insertions(+), 18 deletions(-)
diff --git a/assets/icons/zap_4x.png b/assets/icons/zap_4x.png
Binary files differ.
diff --git a/crates/notedeck/src/lib.rs b/crates/notedeck/src/lib.rs
@@ -57,9 +57,10 @@ pub use user_account::UserAccount;
pub use wallet::{
get_wallet_for_mut, GlobalWallet, Wallet, WalletError, WalletState, WalletType, WalletUIState,
};
+pub use zaps::{AnyZapState, NoteZapTarget, NoteZapTargetOwned, ZapTarget, ZappingError};
// export libs
pub use enostr;
pub use nostrdb;
-pub use zaps::Zaps;
-\ No newline at end of file
+pub use zaps::Zaps;
diff --git a/crates/notedeck/src/zaps/mod.rs b/crates/notedeck/src/zaps/mod.rs
@@ -2,4 +2,4 @@ mod cache;
mod networking;
mod zap;
-pub use cache::Zaps;
+pub use cache::{AnyZapState, NoteZapTarget, NoteZapTargetOwned, ZapTarget, ZappingError, Zaps};
diff --git a/crates/notedeck_columns/src/actionbar.rs b/crates/notedeck_columns/src/actionbar.rs
@@ -4,9 +4,12 @@ use crate::{
timeline::{TimelineCache, TimelineKind},
};
-use enostr::{NoteId, RelayPool};
+use enostr::{NoteId, Pubkey, RelayPool};
use nostrdb::{Ndb, NoteKey, Transaction};
-use notedeck::{NoteCache, UnknownIds};
+use notedeck::{
+ get_wallet_for_mut, Accounts, GlobalWallet, NoteCache, NoteZapTargetOwned, UnknownIds,
+ ZapTarget, ZappingError, Zaps,
+};
use tracing::error;
#[derive(Debug, Eq, PartialEq, Clone)]
@@ -14,6 +17,13 @@ pub enum NoteAction {
Reply(NoteId),
Quote(NoteId),
OpenTimeline(TimelineKind),
+ Zap(ZapAction),
+}
+
+#[derive(Debug, Eq, PartialEq, Clone)]
+pub enum ZapAction {
+ Send(NoteZapTargetOwned),
+ ClearError(NoteZapTargetOwned),
}
pub struct NewNotes {
@@ -35,6 +45,9 @@ impl NoteAction {
note_cache: &mut NoteCache,
pool: &mut RelayPool,
txn: &Transaction,
+ accounts: &mut Accounts,
+ global_wallet: &mut GlobalWallet,
+ zaps: &mut Zaps,
) -> Option<TimelineOpenResult> {
match self {
NoteAction::Reply(note_id) => {
@@ -51,6 +64,31 @@ impl NoteAction {
router.route_to(Route::quote(*note_id));
None
}
+
+ NoteAction::Zap(zap_action) => 's: {
+ let Some(cur_acc) = accounts.get_selected_account_mut() else {
+ break 's None;
+ };
+
+ let sender = cur_acc.key.pubkey;
+
+ match zap_action {
+ ZapAction::Send(target) => {
+ if get_wallet_for_mut(accounts, global_wallet, sender.bytes()).is_some() {
+ send_zap(&sender, zaps, pool, target)
+ } else {
+ zaps.send_error(
+ sender.bytes(),
+ ZapTarget::Note(target.into()),
+ ZappingError::SenderNoWallet,
+ );
+ }
+ }
+ ZapAction::ClearError(target) => clear_zap_error(&sender, zaps, target),
+ }
+
+ None
+ }
}
}
@@ -66,14 +104,39 @@ impl NoteAction {
pool: &mut RelayPool,
txn: &Transaction,
unknown_ids: &mut UnknownIds,
+ accounts: &mut Accounts,
+ global_wallet: &mut GlobalWallet,
+ zaps: &mut Zaps,
) {
let router = columns.column_mut(col).router_mut();
- if let Some(br) = self.execute(ndb, router, timeline_cache, note_cache, pool, txn) {
+ if let Some(br) = self.execute(
+ ndb,
+ router,
+ timeline_cache,
+ note_cache,
+ pool,
+ txn,
+ accounts,
+ global_wallet,
+ zaps,
+ ) {
br.process(ndb, note_cache, txn, timeline_cache, unknown_ids);
}
}
}
+fn send_zap(sender: &Pubkey, zaps: &mut Zaps, pool: &RelayPool, target: &NoteZapTargetOwned) {
+ let default_zap_msats = 10_000; // TODO(kernelkind): allow the user to set this default
+ let zap_target = ZapTarget::Note(target.into());
+
+ let sender_relays: Vec<String> = pool.relays.iter().map(|r| r.url().to_string()).collect();
+ zaps.send_zap(sender.bytes(), sender_relays, zap_target, default_zap_msats);
+}
+
+fn clear_zap_error(sender: &Pubkey, zaps: &mut Zaps, target: &NoteZapTargetOwned) {
+ zaps.clear_error_for(sender.bytes(), ZapTarget::Note(target.into()));
+}
+
impl TimelineOpenResult {
pub fn new_notes(notes: Vec<NoteKey>, id: TimelineKind) -> Self {
Self::NewNotes(NewNotes::new(notes, id))
diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs
@@ -176,6 +176,9 @@ impl RenderNavResponse {
ctx.pool,
&txn,
ctx.unknown_ids,
+ ctx.accounts,
+ ctx.global_wallet,
+ ctx.zaps,
);
}
diff --git a/crates/notedeck_columns/src/ui/note/mod.rs b/crates/notedeck_columns/src/ui/note/mod.rs
@@ -16,7 +16,7 @@ pub use reply::PostReplyView;
pub use reply_description::reply_desc;
use crate::{
- actionbar::NoteAction,
+ actionbar::{NoteAction, ZapAction},
profile::get_display_name,
timeline::{ThreadSelection, TimelineKind},
ui::{self, View},
@@ -26,9 +26,12 @@ use egui::emath::{pos2, Vec2};
use egui::{Id, Label, Pos2, Rect, Response, RichText, Sense};
use enostr::{KeypairUnowned, NoteId, Pubkey};
use nostrdb::{Ndb, Note, NoteKey, Transaction};
-use notedeck::{CachedNote, NoteCache, NotedeckTextStyle};
+use notedeck::{
+ AnyZapState, CachedNote, NoteCache, NoteZapTarget, NoteZapTargetOwned, NotedeckTextStyle,
+ ZapTarget, Zaps,
+};
-use super::profile::preview::one_line_display_name_widget;
+use super::{profile::preview::one_line_display_name_widget, widgets::x_button};
pub struct NoteView<'a, 'd> {
note_context: &'a mut NoteContext<'d>,
@@ -409,7 +412,15 @@ impl<'a, 'd> NoteView<'a, 'd> {
}
if self.options().has_actionbar() {
- if let Some(action) = render_note_actionbar(ui, self.note.id(), note_key).inner
+ if let Some(action) = render_note_actionbar(
+ ui,
+ self.note_context.zaps,
+ self.cur_acc.as_ref(),
+ self.note.id(),
+ self.note.pubkey(),
+ note_key,
+ )
+ .inner
{
note_action = Some(action);
}
@@ -467,8 +478,15 @@ impl<'a, 'd> NoteView<'a, 'd> {
}
if self.options().has_actionbar() {
- if let Some(action) =
- render_note_actionbar(ui, self.note.id(), note_key).inner
+ if let Some(action) = render_note_actionbar(
+ ui,
+ self.note_context.zaps,
+ self.cur_acc.as_ref(),
+ self.note.id(),
+ self.note.pubkey(),
+ note_key,
+ )
+ .inner
{
note_action = Some(action);
}
@@ -586,20 +604,67 @@ fn note_hitbox_clicked(
#[profiling::function]
fn render_note_actionbar(
ui: &mut egui::Ui,
+ zaps: &Zaps,
+ cur_acc: Option<&KeypairUnowned>,
note_id: &[u8; 32],
+ note_pubkey: &[u8; 32],
note_key: NoteKey,
) -> egui::InnerResponse<Option<NoteAction>> {
- ui.horizontal(|ui| {
+ ui.horizontal(|ui| 's: {
let reply_resp = reply_button(ui, note_key);
let quote_resp = quote_repost_button(ui, note_key);
+ let zap_target = ZapTarget::Note(NoteZapTarget {
+ note_id,
+ zap_recipient: note_pubkey,
+ });
+
+ let zap_state = cur_acc.map_or_else(
+ || AnyZapState::None,
+ |kp| zaps.any_zap_state_for(kp.pubkey.bytes(), zap_target),
+ );
+ let zap_resp = cur_acc
+ .filter(|k| k.secret_key.is_some())
+ .map(|_| match &zap_state {
+ AnyZapState::None => ui.add(zap_button(false)),
+ AnyZapState::Pending => ui.spinner(),
+ AnyZapState::LocalOnly | AnyZapState::Confirmed => ui.add(zap_button(true)),
+ AnyZapState::Error(zapping_error) => {
+ let (rect, _) =
+ ui.allocate_at_least(egui::vec2(10.0, 10.0), egui::Sense::click());
+ ui.add(x_button(rect))
+ .on_hover_text(format!("{zapping_error}"))
+ }
+ });
+
+ let to_noteid = |id: &[u8; 32]| NoteId::new(*id);
+
if reply_resp.clicked() {
- Some(NoteAction::Reply(NoteId::new(*note_id)))
- } else if quote_resp.clicked() {
- Some(NoteAction::Quote(NoteId::new(*note_id)))
- } else {
- None
+ break 's Some(NoteAction::Reply(to_noteid(note_id)));
+ }
+
+ if quote_resp.clicked() {
+ break 's Some(NoteAction::Quote(to_noteid(note_id)));
+ }
+
+ let Some(zap_resp) = zap_resp else {
+ break 's None;
+ };
+
+ if !zap_resp.clicked() {
+ break 's None;
+ }
+
+ let target = NoteZapTargetOwned {
+ note_id: to_noteid(note_id),
+ zap_recipient: Pubkey::new(*note_pubkey),
+ };
+
+ if matches!(zap_state, AnyZapState::Error(_)) {
+ break 's Some(NoteAction::Zap(ZapAction::ClearError(target)));
}
+
+ Some(NoteAction::Zap(ZapAction::Send(target)))
})
}
@@ -669,3 +734,29 @@ fn quote_repost_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response {
resp.union(put_resp)
}
+
+fn zap_button(colored: bool) -> impl egui::Widget {
+ move |ui: &mut egui::Ui| -> egui::Response {
+ let img_data = egui::include_image!("../../../../../assets/icons/zap_4x.png");
+
+ let (rect, size, resp) = ui::anim::hover_expand_small(ui, ui.id().with("zap"));
+
+ let mut img = egui::Image::new(img_data).max_width(size);
+
+ if colored {
+ img = img.tint(egui::Color32::from_rgb(0xFF, 0xB7, 0x57));
+ }
+
+ if !colored && !ui.visuals().dark_mode {
+ img = img.tint(egui::Color32::BLACK);
+ }
+
+ // align rect to note contents
+ let expand_size = 5.0; // from hover_expand_small
+ let rect = rect.translate(egui::vec2(-(expand_size / 2.0), 0.0));
+
+ let put_resp = ui.put(rect, img);
+
+ resp.union(put_resp)
+ }
+}