From de9e0e4ca1be1e335dffde4ee923ac5bd9f3972c Mon Sep 17 00:00:00 2001 From: kernelkind Date: Tue, 17 Sep 2024 12:20:29 -0400 Subject: [PATCH 1/6] post quote reposts impl Signed-off-by: kernelkind --- enostr/src/note.rs | 6 +++ src/actionbar.rs | 7 +++ src/draft.rs | 8 +++- src/post.rs | 25 +++++++++++ src/route.rs | 5 +++ src/timeline/route.rs | 37 +++++++++++++++- src/ui/note/contents.rs | 2 +- src/ui/note/mod.rs | 30 +++++++++++++ src/ui/note/post.rs | 63 +++++++++++++++++++++----- src/ui/note/quote_repost.rs | 88 +++++++++++++++++++++++++++++++++++++ src/ui/note/reply.rs | 13 ++++-- src/ui/timeline.rs | 11 ++++- 12 files changed, 275 insertions(+), 20 deletions(-) create mode 100644 src/ui/note/quote_repost.rs diff --git a/enostr/src/note.rs b/enostr/src/note.rs index 61492c9..b41fd58 100644 --- a/enostr/src/note.rs +++ b/enostr/src/note.rs @@ -6,6 +6,8 @@ use std::hash::{Hash, Hasher}; #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] pub struct NoteId([u8; 32]); +static HRP_NOTE: nostr::bech32::Hrp = nostr::bech32::Hrp::parse_unchecked("note"); + impl NoteId { pub fn new(bytes: [u8; 32]) -> Self { NoteId(bytes) @@ -23,6 +25,10 @@ impl NoteId { let evid = NoteId(hex::decode(hex_str)?.as_slice().try_into().unwrap()); Ok(evid) } + + pub fn to_bech(&self) -> Option { + nostr::bech32::encode::(HRP_NOTE, &self.0).ok() + } } /// Event is the struct used to represent a Nostr event diff --git a/src/actionbar.rs b/src/actionbar.rs index ca452b6..7c7c909 100644 --- a/src/actionbar.rs +++ b/src/actionbar.rs @@ -12,6 +12,7 @@ use uuid::Uuid; #[derive(Debug, Eq, PartialEq, Copy, Clone)] pub enum BarAction { Reply(NoteId), + Quote(NoteId), OpenThread(NoteId), } @@ -130,6 +131,12 @@ impl BarAction { BarAction::OpenThread(note_id) => { open_thread(ndb, txn, router, note_cache, pool, threads, note_id.bytes()) } + + BarAction::Quote(note_id) => { + router.route_to(Route::quote(note_id)); + router.navigating = true; + None + } } } diff --git a/src/draft.rs b/src/draft.rs index 3f62b71..5d2413b 100644 --- a/src/draft.rs +++ b/src/draft.rs @@ -8,6 +8,7 @@ pub struct Draft { #[derive(Default)] pub struct Drafts { replies: HashMap<[u8; 32], Draft>, + quotes: HashMap<[u8; 32], Draft>, compose: Draft, } @@ -19,14 +20,19 @@ impl Drafts { pub fn reply_mut(&mut self, id: &[u8; 32]) -> &mut Draft { self.replies.entry(*id).or_default() } + + pub fn quote_mut(&mut self, id: &[u8; 32]) -> &mut Draft { + self.quotes.entry(*id).or_default() + } } -/* pub enum DraftSource<'a> { Compose, Reply(&'a [u8; 32]), // note id + Quote(&'a [u8; 32]), // note id } +/* impl<'a> DraftSource<'a> { pub fn draft(&self, drafts: &'a mut Drafts) -> &'a mut Draft { match self { diff --git a/src/post.rs b/src/post.rs index 3433dbb..323e57a 100644 --- a/src/post.rs +++ b/src/post.rs @@ -89,4 +89,29 @@ impl NewPost { .build() .expect("expected build to work") } + + pub fn to_quote(&self, seckey: &[u8; 32], quoting: &Note) -> Note { + let new_content = format!( + "{}\nnostr:{}", + self.content, + enostr::NoteId::new(*quoting.id()).to_bech().unwrap() + ); + let builder = NoteBuilder::new().kind(1).content(&new_content); + + let builder = builder + .start_tag() + .tag_str("q") + .tag_str(&hex::encode(quoting.id())) + .sign(seckey); + + let builder = builder + .start_tag() + .tag_str("p") + .tag_str(&hex::encode(quoting.pubkey())); + + builder + .sign(seckey) + .build() + .expect("expected build to work") + } } diff --git a/src/route.rs b/src/route.rs index 99e0ac8..33acb34 100644 --- a/src/route.rs +++ b/src/route.rs @@ -39,6 +39,10 @@ impl Route { Route::Timeline(TimelineRoute::Reply(replying_to)) } + pub fn quote(quoting: NoteId) -> Self { + Route::Timeline(TimelineRoute::Quote(quoting)) + } + pub fn accounts() -> Self { Route::Accounts(AccountsRoute::Accounts) } @@ -110,6 +114,7 @@ impl fmt::Display for Route { TimelineRoute::Timeline(name) => write!(f, "{}", name), TimelineRoute::Thread(_id) => write!(f, "Thread"), TimelineRoute::Reply(_id) => write!(f, "Reply"), + TimelineRoute::Quote(_id) => write!(f, "Quote"), }, Route::Relays => write!(f, "Relays"), diff --git a/src/timeline/route.rs b/src/timeline/route.rs index d506004..c023dc5 100644 --- a/src/timeline/route.rs +++ b/src/timeline/route.rs @@ -6,7 +6,10 @@ use crate::{ notecache::NoteCache, thread::Threads, timeline::TimelineId, - ui::{self, note::post::PostResponse}, + ui::{ + self, + note::{post::PostResponse, QuoteRepostView}, + }, }; use enostr::{NoteId, RelayPool}; @@ -17,6 +20,7 @@ pub enum TimelineRoute { Timeline(TimelineId), Thread(NoteId), Reply(NoteId), + Quote(NoteId), } pub enum TimelineRouteResponse { @@ -49,7 +53,7 @@ pub fn render_timeline_route( TimelineRoute::Timeline(timeline_id) => { if show_postbox { if let Some(kp) = accounts.selected_or_first_nsec() { - ui::timeline::postbox_view(ndb, kp, pool, drafts, img_cache, ui); + ui::timeline::postbox_view(ndb, kp, pool, drafts, img_cache, note_cache, ui); } } @@ -109,5 +113,34 @@ pub fn render_timeline_route( None } } + + TimelineRoute::Quote(id) => { + let txn = if let Ok(txn) = Transaction::new(ndb) { + txn + } else { + ui.label("Quote of unknown note"); + return None; + }; + + let note = if let Ok(note) = ndb.get_note_by_id(&txn, id.bytes()) { + note + } else { + ui.label("Quote of unknown note"); + return None; + }; + + let id = egui::Id::new(("post", col, note.key().unwrap())); + if let Some(poster) = accounts.selected_or_first_nsec() { + let response = egui::ScrollArea::vertical().show(ui, |ui| { + QuoteRepostView::new(ndb, poster, pool, note_cache, img_cache, drafts, ¬e) + .id_source(id) + .show(ui) + }); + + Some(TimelineRouteResponse::post(response.inner)) + } else { + None + } + } } } diff --git a/src/ui/note/contents.rs b/src/ui/note/contents.rs index d44fb8c..21eb115 100644 --- a/src/ui/note/contents.rs +++ b/src/ui/note/contents.rs @@ -58,7 +58,7 @@ impl egui::Widget for NoteContents<'_> { /// Render an inline note preview with a border. These are used when /// notes are references within a note -fn render_note_preview( +pub fn render_note_preview( ui: &mut egui::Ui, ndb: &Ndb, note_cache: &mut NoteCache, diff --git a/src/ui/note/mod.rs b/src/ui/note/mod.rs index a9c5b33..afd6c2c 100644 --- a/src/ui/note/mod.rs +++ b/src/ui/note/mod.rs @@ -1,11 +1,13 @@ pub mod contents; pub mod options; pub mod post; +pub mod quote_repost; pub mod reply; pub use contents::NoteContents; pub use options::NoteOptions; pub use post::{PostAction, PostResponse, PostView}; +pub use quote_repost::QuoteRepostView; pub use reply::PostReplyView; use crate::{ @@ -555,9 +557,12 @@ fn render_note_actionbar( ) -> egui::InnerResponse> { ui.horizontal(|ui| { let reply_resp = reply_button(ui, note_key); + let quote_resp = quote_repost_button(ui, note_key); if reply_resp.clicked() { Some(BarAction::Reply(NoteId::new(*note_id))) + } else if quote_resp.clicked() { + Some(BarAction::Quote(NoteId::new(*note_id))) } else { None } @@ -614,3 +619,28 @@ fn repost_icon() -> egui::Image<'static> { let img_data = egui::include_image!("../../../assets/icons/repost_icon_4x.png"); egui::Image::new(img_data) } + +fn quote_repost_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response { + let id = ui.id().with(("quote-anim", note_key)); + let size = 8.0; + let expand_size = 5.0; + let anim_speed = 0.05; + + let (rect, size, resp) = ui::anim::hover_expand(ui, id, size, expand_size, anim_speed); + + let color = if ui.style().visuals.dark_mode { + egui::Color32::WHITE + } else { + egui::Color32::BLACK + }; + + ui.painter_at(rect).text( + rect.center(), + egui::Align2::CENTER_CENTER, + "Q", + egui::FontId::proportional(size + 2.0), + color, + ); + + resp +} diff --git a/src/ui/note/post.rs b/src/ui/note/post.rs index d3b755d..5872dd4 100644 --- a/src/ui/note/post.rs +++ b/src/ui/note/post.rs @@ -1,16 +1,22 @@ -use crate::draft::Draft; +use crate::draft::{Draft, DraftSource}; use crate::imgcache::ImageCache; +use crate::notecache::NoteCache; use crate::post::NewPost; use crate::ui; use crate::ui::{Preview, PreviewConfig, View}; use egui::widgets::text_edit::TextEdit; +use egui::{Frame, Layout}; use enostr::{FilledKeypair, FullKeypair}; use nostrdb::{Config, Ndb, Transaction}; +use super::contents::render_note_preview; + pub struct PostView<'a> { ndb: &'a Ndb, draft: &'a mut Draft, + draft_source: DraftSource<'a>, img_cache: &'a mut ImageCache, + note_cache: &'a mut NoteCache, poster: FilledKeypair<'a>, id_source: Option, } @@ -28,7 +34,9 @@ impl<'a> PostView<'a> { pub fn new( ndb: &'a Ndb, draft: &'a mut Draft, + draft_source: DraftSource<'a>, img_cache: &'a mut ImageCache, + note_cache: &'a mut NoteCache, poster: FilledKeypair<'a>, ) -> Self { let id_source: Option = None; @@ -36,8 +44,10 @@ impl<'a> PostView<'a> { ndb, draft, img_cache, + note_cache, poster, id_source, + draft_source, } } @@ -129,18 +139,41 @@ impl<'a> PostView<'a> { let edit_response = ui.horizontal(|ui| self.editbox(txn, ui)).inner; let action = ui - .with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { - if ui - .add_sized([91.0, 32.0], egui::Button::new("Post now")) - .clicked() - { - Some(PostAction::Post(NewPost::new( - self.draft.buffer.clone(), - self.poster.to_full(), - ))) - } else { - None + .horizontal(|ui| { + if let DraftSource::Quote(id) = self.draft_source { + let avail_size = ui.available_size_before_wrap(); + ui.with_layout(Layout::left_to_right(egui::Align::TOP), |ui| { + Frame::none().show(ui, |ui| { + ui.vertical(|ui| { + ui.set_max_width(avail_size.x * 0.8); + render_note_preview( + ui, + self.ndb, + self.note_cache, + self.img_cache, + txn, + id, + "", + ); + }); + }); + }); } + + ui.with_layout(egui::Layout::right_to_left(egui::Align::BOTTOM), |ui| { + if ui + .add_sized([91.0, 32.0], egui::Button::new("Post now")) + .clicked() + { + Some(PostAction::Post(NewPost::new( + self.draft.buffer.clone(), + self.poster.to_full(), + ))) + } else { + None + } + }) + .inner }) .inner; @@ -161,6 +194,7 @@ mod preview { pub struct PostPreview { ndb: Ndb, img_cache: ImageCache, + note_cache: NoteCache, draft: Draft, poster: FullKeypair, } @@ -172,6 +206,9 @@ mod preview { PostPreview { ndb, img_cache: ImageCache::new(".".into()), + note_cache: NoteCache { + cache: Default::default(), + }, draft: Draft::new(), poster: FullKeypair::generate(), } @@ -184,7 +221,9 @@ mod preview { PostView::new( &self.ndb, &mut self.draft, + DraftSource::Compose, &mut self.img_cache, + &mut self.note_cache, self.poster.to_filled(), ) .ui(&txn, ui); diff --git a/src/ui/note/quote_repost.rs b/src/ui/note/quote_repost.rs new file mode 100644 index 0000000..7add1bd --- /dev/null +++ b/src/ui/note/quote_repost.rs @@ -0,0 +1,88 @@ +use enostr::{FilledKeypair, RelayPool}; +use nostrdb::Ndb; +use tracing::info; + +use crate::{draft::Drafts, imgcache::ImageCache, notecache::NoteCache, ui}; + +use super::{PostAction, PostResponse}; + +pub struct QuoteRepostView<'a> { + ndb: &'a Ndb, + poster: FilledKeypair<'a>, + pool: &'a mut RelayPool, + note_cache: &'a mut NoteCache, + img_cache: &'a mut ImageCache, + drafts: &'a mut Drafts, + quoting_note: &'a nostrdb::Note<'a>, + id_source: Option, +} + +impl<'a> QuoteRepostView<'a> { + pub fn new( + ndb: &'a Ndb, + poster: FilledKeypair<'a>, + pool: &'a mut RelayPool, + note_cache: &'a mut NoteCache, + img_cache: &'a mut ImageCache, + drafts: &'a mut Drafts, + quoting_note: &'a nostrdb::Note<'a>, + ) -> Self { + let id_source: Option = None; + QuoteRepostView { + ndb, + poster, + pool, + note_cache, + img_cache, + drafts, + quoting_note, + id_source, + } + } + + pub fn show(&mut self, ui: &mut egui::Ui) -> PostResponse { + let id = self.id(); + let quoting_note_id = self.quoting_note.id(); + + let post_response = { + let draft = self.drafts.quote_mut(quoting_note_id); + ui::PostView::new( + self.ndb, + draft, + crate::draft::DraftSource::Quote(quoting_note_id), + self.img_cache, + self.note_cache, + self.poster, + ) + .id_source(id) + .ui(self.quoting_note.txn().unwrap(), ui) + }; + + if let Some(action) = &post_response.action { + match action { + PostAction::Post(np) => { + let seckey = self.poster.secret_key.to_secret_bytes(); + + let note = np.to_quote(&seckey, self.quoting_note); + + let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap()); + info!("sending {}", raw_msg); + self.pool.send(&enostr::ClientMessage::raw(raw_msg)); + self.drafts.quote_mut(quoting_note_id).clear(); + } + } + } + + post_response + } + + pub fn id_source(mut self, id: egui::Id) -> Self { + self.id_source = Some(id); + self + } + + pub fn id(&self) -> egui::Id { + self.id_source + .unwrap_or_else(|| egui::Id::new("quote-repost-view")) + } +} diff --git a/src/ui/note/reply.rs b/src/ui/note/reply.rs index b1b7a17..aec7d44 100644 --- a/src/ui/note/reply.rs +++ b/src/ui/note/reply.rs @@ -80,9 +80,16 @@ impl<'a> PostReplyView<'a> { let post_response = { let draft = self.drafts.reply_mut(replying_to); - ui::PostView::new(self.ndb, draft, self.img_cache, self.poster) - .id_source(id) - .ui(self.note.txn().unwrap(), ui) + ui::PostView::new( + self.ndb, + draft, + crate::draft::DraftSource::Reply(replying_to), + self.img_cache, + self.note_cache, + self.poster, + ) + .id_source(id) + .ui(self.note.txn().unwrap(), ui) }; if let Some(action) = &post_response.action { diff --git a/src/ui/timeline.rs b/src/ui/timeline.rs index a9e6854..81f3aa5 100644 --- a/src/ui/timeline.rs +++ b/src/ui/timeline.rs @@ -173,11 +173,20 @@ pub fn postbox_view<'a>( pool: &'a mut RelayPool, drafts: &'a mut Drafts, img_cache: &'a mut ImageCache, + note_cache: &'a mut NoteCache, ui: &'a mut egui::Ui, ) { // show a postbox in the first timeline let txn = Transaction::new(ndb).expect("txn"); - let response = ui::PostView::new(ndb, drafts.compose_mut(), img_cache, key).ui(&txn, ui); + let response = ui::PostView::new( + ndb, + drafts.compose_mut(), + crate::draft::DraftSource::Compose, + img_cache, + note_cache, + key, + ) + .ui(&txn, ui); if let Some(action) = response.action { match action { From 33dcd8ba667430e337644c969261dc1d3b8d44f3 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Tue, 17 Sep 2024 13:09:07 -0400 Subject: [PATCH 2/6] make PostActionExecutor for code reuse Signed-off-by: kernelkind --- src/lib.rs | 1 + src/post_action_executor.rs | 29 +++++++++++++++++++++++++++++ src/ui/note/quote_repost.rs | 30 +++++++++++++++--------------- src/ui/note/reply.rs | 26 ++++++++++++-------------- src/ui/timeline.rs | 23 ++++++++++++----------- 5 files changed, 69 insertions(+), 40 deletions(-) create mode 100644 src/post_action_executor.rs diff --git a/src/lib.rs b/src/lib.rs index 2362a9d..22610e4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,7 @@ mod nav; mod note; mod notecache; mod post; +mod post_action_executor; mod profile; pub mod relay_pool_manager; mod result; diff --git a/src/post_action_executor.rs b/src/post_action_executor.rs new file mode 100644 index 0000000..e26c109 --- /dev/null +++ b/src/post_action_executor.rs @@ -0,0 +1,29 @@ +use enostr::{FilledKeypair, RelayPool}; +use nostrdb::Note; +use tracing::info; + +use crate::{draft::Drafts, post::NewPost, ui::note::PostAction}; + +pub struct PostActionExecutor {} + +impl PostActionExecutor { + pub fn execute<'a>( + poster: &FilledKeypair<'_>, + action: &'a PostAction, + pool: &mut RelayPool, + drafts: &mut Drafts, + get_note: impl Fn(&'a NewPost, &[u8; 32]) -> Note<'a>, + clear_draft: impl Fn(&mut Drafts), + ) { + match action { + PostAction::Post(np) => { + let note = get_note(np, &poster.secret_key.to_secret_bytes()); + + let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap()); + info!("sending {}", raw_msg); + pool.send(&enostr::ClientMessage::raw(raw_msg)); + clear_draft(drafts); + } + } + } +} diff --git a/src/ui/note/quote_repost.rs b/src/ui/note/quote_repost.rs index 7add1bd..8b483e2 100644 --- a/src/ui/note/quote_repost.rs +++ b/src/ui/note/quote_repost.rs @@ -1,10 +1,12 @@ use enostr::{FilledKeypair, RelayPool}; use nostrdb::Ndb; -use tracing::info; -use crate::{draft::Drafts, imgcache::ImageCache, notecache::NoteCache, ui}; +use crate::{ + draft::Drafts, imgcache::ImageCache, notecache::NoteCache, + post_action_executor::PostActionExecutor, ui, +}; -use super::{PostAction, PostResponse}; +use super::PostResponse; pub struct QuoteRepostView<'a> { ndb: &'a Ndb, @@ -59,18 +61,16 @@ impl<'a> QuoteRepostView<'a> { }; if let Some(action) = &post_response.action { - match action { - PostAction::Post(np) => { - let seckey = self.poster.secret_key.to_secret_bytes(); - - let note = np.to_quote(&seckey, self.quoting_note); - - let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap()); - info!("sending {}", raw_msg); - self.pool.send(&enostr::ClientMessage::raw(raw_msg)); - self.drafts.quote_mut(quoting_note_id).clear(); - } - } + PostActionExecutor::execute( + &self.poster, + action, + self.pool, + self.drafts, + |np, seckey| np.to_quote(seckey, self.quoting_note), + |drafts| { + drafts.quote_mut(quoting_note_id).clear(); + }, + ); } post_response diff --git a/src/ui/note/reply.rs b/src/ui/note/reply.rs index aec7d44..e50efbc 100644 --- a/src/ui/note/reply.rs +++ b/src/ui/note/reply.rs @@ -1,11 +1,11 @@ use crate::draft::Drafts; use crate::imgcache::ImageCache; use crate::notecache::NoteCache; +use crate::post_action_executor::PostActionExecutor; use crate::ui; -use crate::ui::note::{PostAction, PostResponse}; +use crate::ui::note::PostResponse; use enostr::{FilledKeypair, RelayPool}; use nostrdb::Ndb; -use tracing::info; pub struct PostReplyView<'a> { ndb: &'a Ndb, @@ -93,18 +93,16 @@ impl<'a> PostReplyView<'a> { }; if let Some(action) = &post_response.action { - match action { - PostAction::Post(np) => { - let seckey = self.poster.secret_key.to_secret_bytes(); - - let note = np.to_reply(&seckey, self.note); - - let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap()); - info!("sending {}", raw_msg); - self.pool.send(&enostr::ClientMessage::raw(raw_msg)); - self.drafts.reply_mut(replying_to).clear(); - } - } + PostActionExecutor::execute( + &self.poster, + action, + self.pool, + self.drafts, + |np, seckey| np.to_reply(seckey, self.note), + |drafts| { + drafts.reply_mut(replying_to).clear(); + }, + ); } // diff --git a/src/ui/timeline.rs b/src/ui/timeline.rs index 81f3aa5..cb17281 100644 --- a/src/ui/timeline.rs +++ b/src/ui/timeline.rs @@ -1,13 +1,14 @@ +use crate::post_action_executor::PostActionExecutor; use crate::{ actionbar::BarAction, column::Columns, draft::Drafts, imgcache::ImageCache, - notecache::NoteCache, timeline::TimelineId, ui, ui::note::PostAction, + notecache::NoteCache, timeline::TimelineId, ui, }; use egui::containers::scroll_area::ScrollBarVisibility; use egui::{Direction, Layout}; use egui_tabs::TabColor; use enostr::{FilledKeypair, RelayPool}; use nostrdb::{Ndb, Transaction}; -use tracing::{debug, error, info, warn}; +use tracing::{debug, error, warn}; pub struct TimelineView<'a> { timeline_id: TimelineId, @@ -189,16 +190,16 @@ pub fn postbox_view<'a>( .ui(&txn, ui); if let Some(action) = response.action { - match action { - PostAction::Post(np) => { - let seckey = key.secret_key.to_secret_bytes(); - let note = np.to_note(&seckey); - let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap()); - info!("sending {}", raw_msg); - pool.send(&enostr::ClientMessage::raw(raw_msg)); + PostActionExecutor::execute( + &key, + &action, + pool, + drafts, + |np, seckey| np.to_note(seckey), + |drafts| { drafts.compose_mut().clear(); - } - } + }, + ); } } From 06336a14ef7879be38d13fd5d6c7887ef87d4a0f Mon Sep 17 00:00:00 2001 From: kernelkind Date: Tue, 17 Sep 2024 17:04:29 -0400 Subject: [PATCH 3/6] add repost button Signed-off-by: kernelkind --- src/ui/note/mod.rs | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/src/ui/note/mod.rs b/src/ui/note/mod.rs index afd6c2c..491905b 100644 --- a/src/ui/note/mod.rs +++ b/src/ui/note/mod.rs @@ -621,26 +621,13 @@ fn repost_icon() -> egui::Image<'static> { } fn quote_repost_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response { - let id = ui.id().with(("quote-anim", note_key)); - let size = 8.0; + let (rect, size, resp) = + ui::anim::hover_expand_small(ui, ui.id().with(("repost_anim", note_key))); + let expand_size = 5.0; - let anim_speed = 0.05; + let rect = rect.translate(egui::vec2(-(expand_size / 2.0), 0.0)); - let (rect, size, resp) = ui::anim::hover_expand(ui, id, size, expand_size, anim_speed); + let put_resp = ui.put(rect, repost_icon().max_width(size)); - let color = if ui.style().visuals.dark_mode { - egui::Color32::WHITE - } else { - egui::Color32::BLACK - }; - - ui.painter_at(rect).text( - rect.center(), - egui::Align2::CENTER_CENTER, - "Q", - egui::FontId::proportional(size + 2.0), - color, - ); - - resp + resp.union(put_resp) } From 6e77d2019716f942f8cdefa3a2ef973bae924af8 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Wed, 18 Sep 2024 11:07:11 -0400 Subject: [PATCH 4/6] address PR comments Signed-off-by: kernelkind --- src/post.rs | 12 ++++-------- src/post_action_executor.rs | 9 ++++----- src/timeline/route.rs | 31 ++++++++++++++++--------------- src/ui/note/post.rs | 4 +--- src/ui/note/quote_repost.rs | 32 ++++++-------------------------- src/ui/note/reply.rs | 7 ++----- src/ui/timeline.rs | 13 +++---------- 7 files changed, 36 insertions(+), 72 deletions(-) diff --git a/src/post.rs b/src/post.rs index 323e57a..ea58f54 100644 --- a/src/post.rs +++ b/src/post.rs @@ -96,20 +96,16 @@ impl NewPost { self.content, enostr::NoteId::new(*quoting.id()).to_bech().unwrap() ); - let builder = NoteBuilder::new().kind(1).content(&new_content); - let builder = builder + NoteBuilder::new() + .kind(1) + .content(&new_content) .start_tag() .tag_str("q") .tag_str(&hex::encode(quoting.id())) - .sign(seckey); - - let builder = builder .start_tag() .tag_str("p") - .tag_str(&hex::encode(quoting.pubkey())); - - builder + .tag_str(&hex::encode(quoting.pubkey())) .sign(seckey) .build() .expect("expected build to work") diff --git a/src/post_action_executor.rs b/src/post_action_executor.rs index e26c109..1cbb577 100644 --- a/src/post_action_executor.rs +++ b/src/post_action_executor.rs @@ -2,18 +2,17 @@ use enostr::{FilledKeypair, RelayPool}; use nostrdb::Note; use tracing::info; -use crate::{draft::Drafts, post::NewPost, ui::note::PostAction}; +use crate::{draft::Draft, post::NewPost, ui::note::PostAction}; pub struct PostActionExecutor {} impl PostActionExecutor { pub fn execute<'a>( - poster: &FilledKeypair<'_>, + poster: FilledKeypair<'_>, action: &'a PostAction, pool: &mut RelayPool, - drafts: &mut Drafts, + draft: &mut Draft, get_note: impl Fn(&'a NewPost, &[u8; 32]) -> Note<'a>, - clear_draft: impl Fn(&mut Drafts), ) { match action { PostAction::Post(np) => { @@ -22,7 +21,7 @@ impl PostActionExecutor { let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap()); info!("sending {}", raw_msg); pool.send(&enostr::ClientMessage::raw(raw_msg)); - clear_draft(drafts); + draft.clear(); } } } diff --git a/src/timeline/route.rs b/src/timeline/route.rs index c023dc5..724e980 100644 --- a/src/timeline/route.rs +++ b/src/timeline/route.rs @@ -4,6 +4,7 @@ use crate::{ draft::Drafts, imgcache::ImageCache, notecache::NoteCache, + post_action_executor::PostActionExecutor, thread::Threads, timeline::TimelineId, ui::{ @@ -115,12 +116,7 @@ pub fn render_timeline_route( } TimelineRoute::Quote(id) => { - let txn = if let Ok(txn) = Transaction::new(ndb) { - txn - } else { - ui.label("Quote of unknown note"); - return None; - }; + let txn = Transaction::new(ndb).expect("txn"); let note = if let Ok(note) = ndb.get_note_by_id(&txn, id.bytes()) { note @@ -130,17 +126,22 @@ pub fn render_timeline_route( }; let id = egui::Id::new(("post", col, note.key().unwrap())); - if let Some(poster) = accounts.selected_or_first_nsec() { - let response = egui::ScrollArea::vertical().show(ui, |ui| { - QuoteRepostView::new(ndb, poster, pool, note_cache, img_cache, drafts, ¬e) - .id_source(id) - .show(ui) - }); - Some(TimelineRouteResponse::post(response.inner)) - } else { - None + let poster = accounts.selected_or_first_nsec()?; + let draft = drafts.quote_mut(note.id()); + + let response = egui::ScrollArea::vertical().show(ui, |ui| { + QuoteRepostView::new(ndb, poster, note_cache, img_cache, draft, ¬e) + .id_source(id) + .show(ui) + }); + + if let Some(action) = &response.inner.action { + PostActionExecutor::execute(poster, action, pool, draft, |np, seckey| { + np.to_quote(seckey, ¬e) + }); } + Some(TimelineRouteResponse::post(response.inner)) } } } diff --git a/src/ui/note/post.rs b/src/ui/note/post.rs index 5872dd4..93b5db5 100644 --- a/src/ui/note/post.rs +++ b/src/ui/note/post.rs @@ -206,9 +206,7 @@ mod preview { PostPreview { ndb, img_cache: ImageCache::new(".".into()), - note_cache: NoteCache { - cache: Default::default(), - }, + note_cache: NoteCache::default(), draft: Draft::new(), poster: FullKeypair::generate(), } diff --git a/src/ui/note/quote_repost.rs b/src/ui/note/quote_repost.rs index 8b483e2..1f1ea34 100644 --- a/src/ui/note/quote_repost.rs +++ b/src/ui/note/quote_repost.rs @@ -1,20 +1,16 @@ -use enostr::{FilledKeypair, RelayPool}; +use enostr::FilledKeypair; use nostrdb::Ndb; -use crate::{ - draft::Drafts, imgcache::ImageCache, notecache::NoteCache, - post_action_executor::PostActionExecutor, ui, -}; +use crate::{draft::Draft, imgcache::ImageCache, notecache::NoteCache, ui}; use super::PostResponse; pub struct QuoteRepostView<'a> { ndb: &'a Ndb, poster: FilledKeypair<'a>, - pool: &'a mut RelayPool, note_cache: &'a mut NoteCache, img_cache: &'a mut ImageCache, - drafts: &'a mut Drafts, + draft: &'a mut Draft, quoting_note: &'a nostrdb::Note<'a>, id_source: Option, } @@ -23,20 +19,18 @@ impl<'a> QuoteRepostView<'a> { pub fn new( ndb: &'a Ndb, poster: FilledKeypair<'a>, - pool: &'a mut RelayPool, note_cache: &'a mut NoteCache, img_cache: &'a mut ImageCache, - drafts: &'a mut Drafts, + draft: &'a mut Draft, quoting_note: &'a nostrdb::Note<'a>, ) -> Self { let id_source: Option = None; QuoteRepostView { ndb, poster, - pool, note_cache, img_cache, - drafts, + draft, quoting_note, id_source, } @@ -47,10 +41,9 @@ impl<'a> QuoteRepostView<'a> { let quoting_note_id = self.quoting_note.id(); let post_response = { - let draft = self.drafts.quote_mut(quoting_note_id); ui::PostView::new( self.ndb, - draft, + self.draft, crate::draft::DraftSource::Quote(quoting_note_id), self.img_cache, self.note_cache, @@ -60,19 +53,6 @@ impl<'a> QuoteRepostView<'a> { .ui(self.quoting_note.txn().unwrap(), ui) }; - if let Some(action) = &post_response.action { - PostActionExecutor::execute( - &self.poster, - action, - self.pool, - self.drafts, - |np, seckey| np.to_quote(seckey, self.quoting_note), - |drafts| { - drafts.quote_mut(quoting_note_id).clear(); - }, - ); - } - post_response } diff --git a/src/ui/note/reply.rs b/src/ui/note/reply.rs index e50efbc..58436b1 100644 --- a/src/ui/note/reply.rs +++ b/src/ui/note/reply.rs @@ -94,14 +94,11 @@ impl<'a> PostReplyView<'a> { if let Some(action) = &post_response.action { PostActionExecutor::execute( - &self.poster, + self.poster, action, self.pool, - self.drafts, + self.drafts.reply_mut(replying_to), |np, seckey| np.to_reply(seckey, self.note), - |drafts| { - drafts.reply_mut(replying_to).clear(); - }, ); } diff --git a/src/ui/timeline.rs b/src/ui/timeline.rs index cb17281..17d22dd 100644 --- a/src/ui/timeline.rs +++ b/src/ui/timeline.rs @@ -190,16 +190,9 @@ pub fn postbox_view<'a>( .ui(&txn, ui); if let Some(action) = response.action { - PostActionExecutor::execute( - &key, - &action, - pool, - drafts, - |np, seckey| np.to_note(seckey), - |drafts| { - drafts.compose_mut().clear(); - }, - ); + PostActionExecutor::execute(key, &action, pool, drafts.compose_mut(), |np, seckey| { + np.to_note(seckey) + }); } } From eadef78543ab9165ad398dbddfb6dd9476f179c4 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Wed, 18 Sep 2024 11:19:24 -0400 Subject: [PATCH 5/6] make views pure Signed-off-by: kernelkind --- src/timeline/route.rs | 33 ++++++++++++++++++++++----------- src/ui/note/reply.rs | 27 ++++++--------------------- src/ui/timeline.rs | 27 +++++++++++---------------- 3 files changed, 39 insertions(+), 48 deletions(-) diff --git a/src/timeline/route.rs b/src/timeline/route.rs index 724e980..a7c5a83 100644 --- a/src/timeline/route.rs +++ b/src/timeline/route.rs @@ -53,8 +53,15 @@ pub fn render_timeline_route( match route { TimelineRoute::Timeline(timeline_id) => { if show_postbox { - if let Some(kp) = accounts.selected_or_first_nsec() { - ui::timeline::postbox_view(ndb, kp, pool, drafts, img_cache, note_cache, ui); + let kp = accounts.selected_or_first_nsec()?; + let draft = drafts.compose_mut(); + let response = + ui::timeline::postbox_view(ndb, kp, draft, img_cache, note_cache, ui); + + if let Some(action) = response.action { + PostActionExecutor::execute(kp, &action, pool, draft, |np, seckey| { + np.to_note(seckey) + }); } } @@ -101,18 +108,22 @@ pub fn render_timeline_route( }; let id = egui::Id::new(("post", col, note.key().unwrap())); + let poster = accounts.selected_or_first_nsec()?; + let draft = drafts.reply_mut(note.id()); - if let Some(poster) = accounts.selected_or_first_nsec() { - let response = egui::ScrollArea::vertical().show(ui, |ui| { - ui::PostReplyView::new(ndb, poster, pool, drafts, note_cache, img_cache, ¬e) - .id_source(id) - .show(ui) + let response = egui::ScrollArea::vertical().show(ui, |ui| { + ui::PostReplyView::new(ndb, poster, draft, note_cache, img_cache, ¬e) + .id_source(id) + .show(ui) + }); + + if let Some(action) = &response.inner.action { + PostActionExecutor::execute(poster, action, pool, draft, |np, seckey| { + np.to_reply(seckey, ¬e) }); - - Some(TimelineRouteResponse::post(response.inner)) - } else { - None } + + Some(TimelineRouteResponse::post(response.inner)) } TimelineRoute::Quote(id) => { diff --git a/src/ui/note/reply.rs b/src/ui/note/reply.rs index 58436b1..a46ac5e 100644 --- a/src/ui/note/reply.rs +++ b/src/ui/note/reply.rs @@ -1,19 +1,17 @@ -use crate::draft::Drafts; +use crate::draft::Draft; use crate::imgcache::ImageCache; use crate::notecache::NoteCache; -use crate::post_action_executor::PostActionExecutor; use crate::ui; use crate::ui::note::PostResponse; -use enostr::{FilledKeypair, RelayPool}; +use enostr::FilledKeypair; use nostrdb::Ndb; pub struct PostReplyView<'a> { ndb: &'a Ndb, poster: FilledKeypair<'a>, - pool: &'a mut RelayPool, note_cache: &'a mut NoteCache, img_cache: &'a mut ImageCache, - drafts: &'a mut Drafts, + draft: &'a mut Draft, note: &'a nostrdb::Note<'a>, id_source: Option, } @@ -22,8 +20,7 @@ impl<'a> PostReplyView<'a> { pub fn new( ndb: &'a Ndb, poster: FilledKeypair<'a>, - pool: &'a mut RelayPool, - drafts: &'a mut Drafts, + draft: &'a mut Draft, note_cache: &'a mut NoteCache, img_cache: &'a mut ImageCache, note: &'a nostrdb::Note<'a>, @@ -32,8 +29,7 @@ impl<'a> PostReplyView<'a> { PostReplyView { ndb, poster, - pool, - drafts, + draft, note, note_cache, img_cache, @@ -79,10 +75,9 @@ impl<'a> PostReplyView<'a> { let rect_before_post = ui.min_rect(); let post_response = { - let draft = self.drafts.reply_mut(replying_to); ui::PostView::new( self.ndb, - draft, + self.draft, crate::draft::DraftSource::Reply(replying_to), self.img_cache, self.note_cache, @@ -92,16 +87,6 @@ impl<'a> PostReplyView<'a> { .ui(self.note.txn().unwrap(), ui) }; - if let Some(action) = &post_response.action { - PostActionExecutor::execute( - self.poster, - action, - self.pool, - self.drafts.reply_mut(replying_to), - |np, seckey| np.to_reply(seckey, self.note), - ); - } - // // reply line // diff --git a/src/ui/timeline.rs b/src/ui/timeline.rs index 17d22dd..bc1e02b 100644 --- a/src/ui/timeline.rs +++ b/src/ui/timeline.rs @@ -1,15 +1,17 @@ -use crate::post_action_executor::PostActionExecutor; +use crate::draft::Draft; use crate::{ - actionbar::BarAction, column::Columns, draft::Drafts, imgcache::ImageCache, - notecache::NoteCache, timeline::TimelineId, ui, + actionbar::BarAction, column::Columns, imgcache::ImageCache, notecache::NoteCache, + timeline::TimelineId, ui, }; use egui::containers::scroll_area::ScrollBarVisibility; use egui::{Direction, Layout}; use egui_tabs::TabColor; -use enostr::{FilledKeypair, RelayPool}; +use enostr::FilledKeypair; use nostrdb::{Ndb, Transaction}; use tracing::{debug, error, warn}; +use super::note::PostResponse; + pub struct TimelineView<'a> { timeline_id: TimelineId, columns: &'a mut Columns, @@ -171,29 +173,22 @@ fn timeline_ui( pub fn postbox_view<'a>( ndb: &'a Ndb, key: FilledKeypair<'a>, - pool: &'a mut RelayPool, - drafts: &'a mut Drafts, + draft: &'a mut Draft, img_cache: &'a mut ImageCache, note_cache: &'a mut NoteCache, ui: &'a mut egui::Ui, -) { +) -> PostResponse { // show a postbox in the first timeline let txn = Transaction::new(ndb).expect("txn"); - let response = ui::PostView::new( + ui::PostView::new( ndb, - drafts.compose_mut(), + draft, crate::draft::DraftSource::Compose, img_cache, note_cache, key, ) - .ui(&txn, ui); - - if let Some(action) = response.action { - PostActionExecutor::execute(key, &action, pool, drafts.compose_mut(), |np, seckey| { - np.to_note(seckey) - }); - } + .ui(&txn, ui) } fn tabs_ui(ui: &mut egui::Ui) -> i32 { From 0a08ae92d9b33901b1c7d714d6fcbba748d17155 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Wed, 18 Sep 2024 11:36:07 -0400 Subject: [PATCH 6/6] minor cleanup Signed-off-by: kernelkind --- src/ui/note/quote_repost.rs | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/ui/note/quote_repost.rs b/src/ui/note/quote_repost.rs index 1f1ea34..50ac183 100644 --- a/src/ui/note/quote_repost.rs +++ b/src/ui/note/quote_repost.rs @@ -40,20 +40,16 @@ impl<'a> QuoteRepostView<'a> { let id = self.id(); let quoting_note_id = self.quoting_note.id(); - let post_response = { - ui::PostView::new( - self.ndb, - self.draft, - crate::draft::DraftSource::Quote(quoting_note_id), - self.img_cache, - self.note_cache, - self.poster, - ) - .id_source(id) - .ui(self.quoting_note.txn().unwrap(), ui) - }; - - post_response + ui::PostView::new( + self.ndb, + self.draft, + crate::draft::DraftSource::Quote(quoting_note_id), + self.img_cache, + self.note_cache, + self.poster, + ) + .id_source(id) + .ui(self.quoting_note.txn().unwrap(), ui) } pub fn id_source(mut self, id: egui::Id) -> Self {