From cfbd601196a5657c10a89cb4b183b1155d823236 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Sun, 30 Mar 2025 15:11:52 -0400 Subject: [PATCH] note zap button Signed-off-by: kernelkind --- assets/icons/zap_4x.png | Bin 1122 -> 866 bytes crates/notedeck/src/lib.rs | 3 +- crates/notedeck/src/zaps/mod.rs | 2 +- crates/notedeck_columns/src/actionbar.rs | 69 ++++++++++++- crates/notedeck_columns/src/nav.rs | 3 + crates/notedeck_columns/src/ui/note/mod.rs | 115 ++++++++++++++++++--- 6 files changed, 175 insertions(+), 17 deletions(-) diff --git a/assets/icons/zap_4x.png b/assets/icons/zap_4x.png index b71e5059b693662b1e08ea60c2d56b58b7070928..cc231965043f8dc83cff11edcbd986509b59cb00 100644 GIT binary patch delta 829 zcmV-D1H$~`2;v4IiBL{Q4GJ0x0000DNk~Le0000q0000y2nGNE04Xb(hLIsOe**VO zL_t(|0qvUIaoaEqfZ<->n+?JZ>Jicjnr`4u5N84_6V#a??F8ut?gq&Q;s#D9KtW3n z3Qd5d1yR&pzdN1oLXaUO^W%>V@SnO_!3-ecmTsTvdZX)cwOUzNDwNChPLwf6`m{3rb9$P(MDNBViVn03~zP5BeB4pv2T~$c+t? z5cdYPBN#2j71T_`Xg)`%8D})i;{b}Njv-7lnva^Y9cl^nHRh@$z*$H8S(E?`#hKaSH%*UKyO6HDkjS%uc z$@~ud-7xXj)aC}pW8IZ?*n$~G3)DSe8Pc}$m|--gev5^Op37l3Pq+>nCd9(I1pGD- z3Xvj(T%(1Em8&5pgqImDf5amcg8j&0XEf>P$r-JIM6g@4ZRq+0<5~At`ah41z1dDW zu@lsh>ljy|6_R;@nMmkc>sQEHqm{-z%&flQ`8NYYjx#6o17%KQYBi`}=++8tzJku~ z(y9Iz2_?BL>}=ZzA@|hY-mkR9vTPwOncGM^4Og*rLTy4aqS1)6eEY~9P?3KL6OML+q3UQ800000NkvXX Hu0mjfG4p}w delta 1087 zcmV-F1i<^^2I2@IiBL{Q4GJ0x0000DNk~Le0000$0000$2nGNE0IF$m-jN|Re*^bP zL_t(|0qvXLQ5!K3#@C16lT?6GL7Wco0MCIAOa<5##Hj$N0H`2-=a1j*5hQpyJBQ`F; zd_F&adU_hI6nTg+v=X7B4T9jmf7XI$9LmOI#Kr_b@W%*rs#L!tBQ_!cf*&CG(}pI0 z$Ov-*5Ihg@?~!Jnj4%}d!M{iFbG&s;?a=0)j4%}dft^z%c&6X)FUSZp0od{Ja*){E zlMyC)Kydui#O90L-Q5plMDY=j;P@}-^?GLWpD8LpqT{~+?aeNMOfdmce;vOLMt~GbfAG-Hboa(W zUgBhvj}>;6r&>MgeGq?Kq@fbh`53E44$Sw&C!`uh3+!mFOt_G+}z=L2dS#~Sdc zMDR_i?W|4kj4FWj;v_}V<3z_lqL!!vK*>A2%*YPkRGGu^B^svyD+;r_BmM z1?>0ZOFF)IJhH_p<(_|~IDVUZh7_y?5PP3<01L;LuzpNV9oiO-FVSgdXbqfH z7}+Gr=J*oUe^X$)W&5m-FJUTxmIG+yc6{+W$Pg_DkSO8!5|)q3syRTNQjRZSIRac_ zyJC(nVJSdL?-zA^2{Qqja)3Z_$Cr3DQ^3OU#qT8}nl6D>jxS**04{;z(6)4Z2{QpO z#5qX6TE+{mmX0rB`k$k`(u2j0ad;-+XUa~{8*h~56))_KU3-C!6(#@x002ovPDHLk FV1jae0o(up diff --git a/crates/notedeck/src/lib.rs b/crates/notedeck/src/lib.rs index e1034ae..df88efe 100644 --- 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 index 3f2b881..f1fd6f7 100644 --- 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 index db67651..82718a1 100644 --- 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 { 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 = 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, 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 index a18323b..67143c4 100644 --- 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 index 88073b0..ee8f829 100644 --- 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> { - 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) + } +}