notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 7f234935cc537780574fa0d6821998aee64dc6dd
parent d97c957e677723f7d20d2e6d233016ee137c1f56
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 19 Nov 2024 11:07:42 -0800

refactor: unify note, post and nav actions

There was a bunch of redundant responses. Let's unify them under
the RenderNavAction enum. We unify all action processing under this
type.

This also centralizes all of our side effects into a single function
instead of scattering them everywhere

Diffstat:
Msrc/actionbar.rs | 37+++++++++++++++++++++----------------
Msrc/app.rs | 14+++++---------
Msrc/column.rs | 3---
Msrc/draft.rs | 26+++++++++-----------------
Msrc/nav.rs | 349+++++++++++++++++++++++++++++++++++++++----------------------------------------
Msrc/route.rs | 13++++++++-----
Msrc/timeline/route.rs | 123+++++++++++++++++++++++++------------------------------------------------------
Msrc/ui/note/contents.rs | 10+++++-----
Msrc/ui/note/mod.rs | 62+++++++++++++++++++++++++++++++++-----------------------------
Msrc/ui/note/post.rs | 82++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Msrc/ui/note/quote_repost.rs | 6+++---
Msrc/ui/note/reply.rs | 6+++---
Msrc/ui/profile/mod.rs | 4++--
Msrc/ui/thread.rs | 8++++----
Msrc/ui/timeline.rs | 25+++++++++++--------------
15 files changed, 368 insertions(+), 400 deletions(-)

diff --git a/src/actionbar.rs b/src/actionbar.rs @@ -1,7 +1,9 @@ use crate::{ + column::Columns, note::NoteRef, notecache::NoteCache, notes_holder::{NotesHolder, NotesHolderStorage}, + profile::Profile, route::{Route, Router}, thread::Thread, }; @@ -9,16 +11,11 @@ use enostr::{NoteId, Pubkey, RelayPool}; use nostrdb::{Ndb, Transaction}; #[derive(Debug, Eq, PartialEq, Copy, Clone)] -pub enum BarAction { +pub enum NoteAction { Reply(NoteId), Quote(NoteId), OpenThread(NoteId), -} - -#[derive(Default)] -pub struct NoteActionResponse { - pub bar_action: Option<BarAction>, - pub open_profile: Option<Pubkey>, + OpenProfile(Pubkey), } pub struct NewNotes { @@ -50,47 +47,55 @@ fn open_thread( Thread::open(ndb, note_cache, txn, pool, threads, root_id) } -impl BarAction { +impl NoteAction { #[allow(clippy::too_many_arguments)] pub fn execute( self, ndb: &Ndb, router: &mut Router<Route>, threads: &mut NotesHolderStorage<Thread>, + profiles: &mut NotesHolderStorage<Profile>, note_cache: &mut NoteCache, pool: &mut RelayPool, txn: &Transaction, ) -> Option<NotesHolderResult> { match self { - BarAction::Reply(note_id) => { + NoteAction::Reply(note_id) => { router.route_to(Route::reply(note_id)); - router.navigating = true; None } - BarAction::OpenThread(note_id) => { + NoteAction::OpenThread(note_id) => { open_thread(ndb, txn, router, note_cache, pool, threads, note_id.bytes()) } - BarAction::Quote(note_id) => { + NoteAction::OpenProfile(pubkey) => { + router.route_to(Route::profile(pubkey)); + Profile::open(ndb, note_cache, txn, pool, profiles, pubkey.bytes()) + } + + NoteAction::Quote(note_id) => { router.route_to(Route::quote(note_id)); - router.navigating = true; None } } } - /// Execute the BarAction and process the BarResult + /// Execute the NoteAction and process the NotesHolderResult + #[allow(clippy::too_many_arguments)] pub fn execute_and_process_result( self, ndb: &Ndb, - router: &mut Router<Route>, + columns: &mut Columns, + col: usize, threads: &mut NotesHolderStorage<Thread>, + profiles: &mut NotesHolderStorage<Profile>, note_cache: &mut NoteCache, pool: &mut RelayPool, txn: &Transaction, ) { - if let Some(br) = self.execute(ndb, router, threads, note_cache, pool, txn) { + let router = columns.column_mut(col).router_mut(); + if let Some(br) = self.execute(ndb, router, threads, profiles, note_cache, pool, txn) { br.process(ndb, note_cache, txn, threads); } } diff --git a/src/app.rs b/src/app.rs @@ -745,9 +745,7 @@ fn render_damus_mobile(ctx: &egui::Context, app: &mut Damus) { main_panel(&ctx.style(), ui::is_narrow(ctx)).show(ctx, |ui| { if !app.columns.columns().is_empty() { - if let Some(r) = nav::render_nav(0, app, ui) { - r.process_nav_response(&app.path, &mut app.columns) - } + nav::render_nav(0, app, ui).process_render_nav_response(app); } }); } @@ -824,13 +822,11 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus) { ); }); - let mut nav_resp: Option<nav::RenderNavResponse> = None; + let mut responses = Vec::with_capacity(app.columns.num_columns()); for col_index in 0..app.columns.num_columns() { strip.cell(|ui| { let rect = ui.available_rect_before_wrap(); - if let Some(r) = nav::render_nav(col_index, app, ui) { - nav_resp = Some(r); - } + responses.push(nav::render_nav(col_index, app, ui)); // vertical line ui.painter().vline( @@ -843,8 +839,8 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus) { //strip.cell(|ui| timeline::timeline_view(ui, app, timeline_ind)); } - if let Some(r) = nav_resp { - r.process_nav_response(&app.path, &mut app.columns); + for response in responses { + response.process_render_nav_response(app); } }); } diff --git a/src/column.rs b/src/column.rs @@ -261,9 +261,6 @@ impl SerializableColumns { Route::Timeline(TimelineRoute::Thread(_thread)) => { // TODO: open thread before pushing route } - Route::Profile(_profile) => { - // TODO: open profile before pushing route - } _ => routes.push(*route), } } diff --git a/src/draft.rs b/src/draft.rs @@ -1,3 +1,4 @@ +use crate::ui::note::PostType; use std::collections::HashMap; #[derive(Default)] @@ -17,6 +18,14 @@ impl Drafts { &mut self.compose } + pub fn get_from_post_type(&mut self, post_type: &PostType) -> &mut Draft { + match post_type { + PostType::New => self.compose_mut(), + PostType::Quote(note_id) => self.quote_mut(note_id.bytes()), + PostType::Reply(note_id) => self.reply_mut(note_id.bytes()), + } + } + pub fn reply_mut(&mut self, id: &[u8; 32]) -> &mut Draft { self.replies.entry(*id).or_default() } @@ -26,23 +35,6 @@ impl Drafts { } } -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 { - DraftSource::Compose => drafts.compose_mut(), - DraftSource::Reply(id) => drafts.reply_mut(id), - } - } -} -*/ - impl Draft { pub fn new() -> Self { Draft::default() diff --git a/src/nav.rs b/src/nav.rs @@ -1,23 +1,23 @@ use crate::{ accounts::render_accounts_route, + actionbar::NoteAction, app_style::{get_font_size, NotedeckTextStyle}, - column::Columns, fonts::NamedFontFamily, notes_holder::NotesHolder, profile::Profile, relay_pool_manager::RelayPoolManager, route::Route, - storage::{self, DataPath}, + storage::{self}, thread::Thread, timeline::{ - route::{render_profile_route, render_timeline_route, AfterRouteExecution, TimelineRoute}, + route::{render_timeline_route, TimelineRoute}, Timeline, }, ui::{ self, add_column::render_add_column_routes, anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, - note::PostAction, + note::{PostAction, PostType}, support::SupportView, RelayView, View, }, @@ -25,32 +25,135 @@ use crate::{ }; use egui::{pos2, Color32, InnerResponse, Stroke}; -use egui_nav::{Nav, NavAction, TitleBarResponse}; +use egui_nav::{Nav, NavAction, NavResponse, TitleBarResponse}; use nostrdb::{Ndb, Transaction}; use tracing::{error, info}; -pub enum RenderNavResponse { - ColumnChanged, - RemoveColumn(usize), +pub enum RenderNavAction { + PostAction(PostAction), + NoteAction(NoteAction), +} + +impl From<PostAction> for RenderNavAction { + fn from(post_action: PostAction) -> Self { + Self::PostAction(post_action) + } +} + +impl From<NoteAction> for RenderNavAction { + fn from(note_action: NoteAction) -> RenderNavAction { + Self::NoteAction(note_action) + } +} + +pub struct RenderNavResponse { + column: usize, + response: NavResponse<Option<RenderNavAction>, TitleResponse>, } impl RenderNavResponse { - pub fn process_nav_response(&self, path: &DataPath, columns: &mut Columns) { - match self { - RenderNavResponse::ColumnChanged => { - storage::save_columns(path, columns.as_serializable_columns()); + #[allow(private_interfaces)] + pub fn new( + column: usize, + response: NavResponse<Option<RenderNavAction>, TitleResponse>, + ) -> Self { + RenderNavResponse { column, response } + } + + pub fn process_render_nav_response(&self, app: &mut Damus) { + let mut col_changed: bool = false; + let col = self.column; + + if let Some(action) = &self.response.inner { + // start returning when we're finished posting + match action { + RenderNavAction::PostAction(post_action) => { + let txn = Transaction::new(&app.ndb).expect("txn"); + let _ = post_action.execute(&app.ndb, &txn, &mut app.pool, &mut app.drafts); + app.columns_mut().column_mut(col).router_mut().go_back(); + } + + RenderNavAction::NoteAction(note_action) => { + let txn = Transaction::new(&app.ndb).expect("txn"); + + note_action.execute_and_process_result( + &app.ndb, + &mut app.columns, + col, + &mut app.threads, + &mut app.profiles, + &mut app.note_cache, + &mut app.pool, + &txn, + ); + } + } + } + + if let Some(NavAction::Returned) = self.response.action { + let r = app.columns_mut().column_mut(col).router_mut().pop(); + let txn = Transaction::new(&app.ndb).expect("txn"); + if let Some(Route::Timeline(TimelineRoute::Thread(id))) = r { + let root_id = { + crate::note::root_note_id_from_selected_id( + &app.ndb, + &mut app.note_cache, + &txn, + id.bytes(), + ) + }; + Thread::unsubscribe_locally( + &txn, + &app.ndb, + &mut app.note_cache, + &mut app.threads, + &mut app.pool, + root_id, + ); + } + + if let Some(Route::Timeline(TimelineRoute::Profile(pubkey))) = r { + Profile::unsubscribe_locally( + &txn, + &app.ndb, + &mut app.note_cache, + &mut app.profiles, + &mut app.pool, + pubkey.bytes(), + ); } + col_changed = true; + } else if let Some(NavAction::Navigated) = self.response.action { + let cur_router = app.columns_mut().column_mut(col).router_mut(); + cur_router.navigating = false; + if cur_router.is_replacing() { + cur_router.remove_previous_routes(); + } + col_changed = true; + } - RenderNavResponse::RemoveColumn(col) => { - columns.delete_column(*col); - storage::save_columns(path, columns.as_serializable_columns()); + if let Some(title_response) = &self.response.title_response { + match title_response { + TitleResponse::RemoveColumn => { + let tl = app.columns().find_timeline_for_column_index(col); + if let Some(timeline) = tl { + unsubscribe_timeline(app.ndb(), timeline); + } + + app.columns_mut().delete_column(col); + col_changed = true; + } } } + + if col_changed { + storage::save_columns(&app.path, app.columns().as_serializable_columns()); + } } } -pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) -> Option<RenderNavResponse> { - let mut resp: Option<RenderNavResponse> = None; +#[must_use = "RenderNavResponse must be handled by calling .process_render_nav_response(..)"] +pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) -> RenderNavResponse { let col_id = app.columns.get_column_id_at_index(col); // TODO(jb55): clean up this router_mut mess by using Router<R> in egui-nav directly let routes = app @@ -61,186 +164,78 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) -> Option<Rend .iter() .map(|r| r.get_titled_route(&app.columns, &app.ndb)) .collect(); + let nav_response = Nav::new(routes) .navigating(app.columns_mut().column_mut(col).router_mut().navigating) .returning(app.columns_mut().column_mut(col).router_mut().returning) .id_source(egui::Id::new(col_id)) .title(48.0, title_bar) - .show_mut(ui, |ui, nav| { - let column = app.columns.column_mut(col); - match &nav.top().route { - Route::Timeline(tlr) => render_timeline_route( + .show_mut(ui, |ui, nav| match &nav.top().route { + Route::Timeline(tlr) => render_timeline_route( + &app.ndb, + &mut app.columns, + &mut app.drafts, + &mut app.img_cache, + &mut app.unknown_ids, + &mut app.note_cache, + &mut app.threads, + &mut app.profiles, + &mut app.accounts, + *tlr, + col, + app.textmode, + ui, + ), + Route::Accounts(amr) => { + let action = render_accounts_route( + ui, &app.ndb, + col, &mut app.columns, - &mut app.pool, - &mut app.drafts, &mut app.img_cache, - &mut app.unknown_ids, - &mut app.note_cache, - &mut app.threads, &mut app.accounts, - *tlr, - col, - app.textmode, - ui, - ), - Route::Accounts(amr) => { - let action = render_accounts_route( - ui, - &app.ndb, - col, - &mut app.columns, - &mut app.img_cache, - &mut app.accounts, - &mut app.view_state.login, - *amr, - ); - let txn = Transaction::new(&app.ndb).expect("txn"); - action.process_action(&mut app.unknown_ids, &app.ndb, &txn); - None - } - Route::Relays => { - let manager = RelayPoolManager::new(app.pool_mut()); - RelayView::new(manager).ui(ui); - None - } - Route::ComposeNote => { - let kp = app.accounts.selected_or_first_nsec()?; - let draft = app.drafts.compose_mut(); - - let txn = nostrdb::Transaction::new(&app.ndb).expect("txn"); - let post_response = ui::PostView::new( - &app.ndb, - draft, - crate::draft::DraftSource::Compose, - &mut app.img_cache, - &mut app.note_cache, - kp, - ) - .ui(&txn, ui); - - if let Some(action) = post_response.action { - PostAction::execute(kp, &action, &mut app.pool, draft, |np, seckey| { - np.to_note(seckey) - }); - column.router_mut().go_back(); - } - - None - } - Route::AddColumn(route) => { - render_add_column_routes(ui, app, col, route); - - None - } + &mut app.view_state.login, + *amr, + ); + let txn = Transaction::new(&app.ndb).expect("txn"); + action.process_action(&mut app.unknown_ids, &app.ndb, &txn); + None + } + Route::Relays => { + let manager = RelayPoolManager::new(app.pool_mut()); + RelayView::new(manager).ui(ui); + None + } + Route::ComposeNote => { + let kp = app.accounts.selected_or_first_nsec()?; + let draft = app.drafts.compose_mut(); - Route::Profile(pubkey) => render_profile_route( - pubkey, + let txn = nostrdb::Transaction::new(&app.ndb).expect("txn"); + let post_response = ui::PostView::new( &app.ndb, - &mut app.columns, - &mut app.profiles, - &mut app.pool, + draft, + PostType::New, &mut app.img_cache, &mut app.note_cache, - &mut app.threads, - col, - ui, - ), - Route::Support => { - SupportView::new(&mut app.support).show(ui); - None - } - } - }); + kp, + ) + .ui(&txn, ui); - if let Some(after_route_execution) = nav_response.inner { - // start returning when we're finished posting - match after_route_execution { - AfterRouteExecution::Post(resp) => { - if let Some(action) = resp.action { - match action { - PostAction::Post(_) => { - app.columns_mut().column_mut(col).router_mut().returning = true; - } - } - } + post_response.action.map(Into::into) } + Route::AddColumn(route) => { + render_add_column_routes(ui, app, col, route); - AfterRouteExecution::OpenProfile(pubkey) => { - app.columns - .column_mut(col) - .router_mut() - .route_to(Route::Profile(pubkey)); - let txn = Transaction::new(&app.ndb).expect("txn"); - if let Some(res) = Profile::open( - &app.ndb, - &mut app.note_cache, - &txn, - &mut app.pool, - &mut app.profiles, - pubkey.bytes(), - ) { - res.process(&app.ndb, &mut app.note_cache, &txn, &mut app.profiles); - } + None } - } - } - if let Some(NavAction::Returned) = nav_response.action { - let r = app.columns_mut().column_mut(col).router_mut().pop(); - let txn = Transaction::new(&app.ndb).expect("txn"); - if let Some(Route::Timeline(TimelineRoute::Thread(id))) = r { - let root_id = { - crate::note::root_note_id_from_selected_id( - &app.ndb, - &mut app.note_cache, - &txn, - id.bytes(), - ) - }; - Thread::unsubscribe_locally( - &txn, - &app.ndb, - &mut app.note_cache, - &mut app.threads, - &mut app.pool, - root_id, - ); - } - - if let Some(Route::Profile(pubkey)) = r { - Profile::unsubscribe_locally( - &txn, - &app.ndb, - &mut app.note_cache, - &mut app.profiles, - &mut app.pool, - pubkey.bytes(), - ); - } - resp = Some(RenderNavResponse::ColumnChanged) - } else if let Some(NavAction::Navigated) = nav_response.action { - let cur_router = app.columns_mut().column_mut(col).router_mut(); - cur_router.navigating = false; - if cur_router.is_replacing() { - cur_router.remove_previous_routes(); - } - resp = Some(RenderNavResponse::ColumnChanged) - } - - if let Some(title_response) = nav_response.title_response { - match title_response { - TitleResponse::RemoveColumn => { - let tl = app.columns().find_timeline_for_column_index(col); - if let Some(timeline) = tl { - unsubscribe_timeline(app.ndb(), timeline); - } - resp = Some(RenderNavResponse::RemoveColumn(col)) + Route::Support => { + SupportView::new(&mut app.support).show(ui); + None } - } - } + }); - resp + RenderNavResponse::new(col, nav_response) } fn unsubscribe_timeline(ndb: &Ndb, timeline: &Timeline) { diff --git a/src/route.rs b/src/route.rs @@ -21,7 +21,6 @@ pub enum Route { Relays, ComposeNote, AddColumn(AddColumnRoute), - Profile(Pubkey), Support, } @@ -58,6 +57,10 @@ impl Route { Route::Timeline(TimelineRoute::Thread(thread_root)) } + pub fn profile(pubkey: Pubkey) -> Self { + Route::Timeline(TimelineRoute::Profile(pubkey)) + } + pub fn reply(replying_to: NoteId) -> Self { Route::Timeline(TimelineRoute::Reply(replying_to)) } @@ -92,6 +95,9 @@ impl Route { TimelineRoute::Quote(id) => { format!("{}'s Quote", get_note_users_displayname_string(ndb, id)) } + TimelineRoute::Profile(pubkey) => { + format!("{}'s Profile", get_profile_displayname_string(ndb, pubkey)) + } }, Route::Relays => "Relays".to_owned(), @@ -108,9 +114,6 @@ impl Route { "Add External Notifications Column".to_owned() } }, - Route::Profile(pubkey) => { - format!("{}'s Profile", get_profile_displayname_string(ndb, pubkey)) - } Route::Support => "Damus Support".to_owned(), }; @@ -207,6 +210,7 @@ impl fmt::Display for Route { Route::Timeline(tlr) => match tlr { TimelineRoute::Timeline(name) => write!(f, "{}", name), TimelineRoute::Thread(_id) => write!(f, "Thread"), + TimelineRoute::Profile(_id) => write!(f, "Profile"), TimelineRoute::Reply(_id) => write!(f, "Reply"), TimelineRoute::Quote(_id) => write!(f, "Quote"), }, @@ -220,7 +224,6 @@ impl fmt::Display for Route { Route::ComposeNote => write!(f, "Compose Note"), Route::AddColumn(_) => write!(f, "Add Column"), - Route::Profile(_) => write!(f, "Profile"), Route::Support => write!(f, "Support"), } } diff --git a/src/timeline/route.rs b/src/timeline/route.rs @@ -3,6 +3,7 @@ use crate::{ column::Columns, draft::Drafts, imgcache::ImageCache, + nav::RenderNavAction, notecache::NoteCache, notes_holder::NotesHolderStorage, profile::Profile, @@ -10,53 +11,40 @@ use crate::{ timeline::{TimelineId, TimelineKind}, ui::{ self, - note::{ - post::{PostAction, PostResponse}, - NoteOptions, QuoteRepostView, - }, + note::{NoteOptions, QuoteRepostView}, profile::ProfileView, }, unknowns::UnknownIds, }; -use enostr::{NoteId, Pubkey, RelayPool}; +use enostr::{NoteId, Pubkey}; use nostrdb::{Ndb, Transaction}; #[derive(Debug, Eq, PartialEq, Clone, Copy, serde::Serialize, serde::Deserialize)] pub enum TimelineRoute { Timeline(TimelineId), Thread(NoteId), + Profile(Pubkey), Reply(NoteId), Quote(NoteId), } -pub enum AfterRouteExecution { - Post(PostResponse), - OpenProfile(Pubkey), -} - -impl AfterRouteExecution { - pub fn post(post: PostResponse) -> Self { - AfterRouteExecution::Post(post) - } -} - #[allow(clippy::too_many_arguments)] pub fn render_timeline_route( ndb: &Ndb, columns: &mut Columns, - pool: &mut RelayPool, drafts: &mut Drafts, img_cache: &mut ImageCache, unknown_ids: &mut UnknownIds, note_cache: &mut NoteCache, threads: &mut NotesHolderStorage<Thread>, + profiles: &mut NotesHolderStorage<Profile>, accounts: &mut Accounts, route: TimelineRoute, col: usize, textmode: bool, ui: &mut egui::Ui, -) -> Option<AfterRouteExecution> { +) -> Option<RenderNavAction> { match route { TimelineRoute::Timeline(timeline_id) => { let note_options = { @@ -71,7 +59,7 @@ pub fn render_timeline_route( options }; - let timeline_response = ui::TimelineView::new( + let note_action = ui::TimelineView::new( timeline_id, columns, ndb, @@ -81,42 +69,21 @@ pub fn render_timeline_route( ) .ui(ui); - if let Some(bar_action) = timeline_response.bar_action { - let txn = Transaction::new(ndb).expect("txn"); - let mut cur_column = columns.columns_mut(); - let router = cur_column[col].router_mut(); - - bar_action.execute_and_process_result(ndb, router, threads, note_cache, pool, &txn); - } - - timeline_response - .open_profile - .map(AfterRouteExecution::OpenProfile) + note_action.map(RenderNavAction::NoteAction) } - TimelineRoute::Thread(id) => { - let timeline_response = ui::ThreadView::new( - threads, - ndb, - note_cache, - unknown_ids, - img_cache, - id.bytes(), - textmode, - ) - .id_source(egui::Id::new(("threadscroll", col))) - .ui(ui); - if let Some(bar_action) = timeline_response.bar_action { - let txn = Transaction::new(ndb).expect("txn"); - let mut cur_column = columns.columns_mut(); - let router = cur_column[col].router_mut(); - bar_action.execute_and_process_result(ndb, router, threads, note_cache, pool, &txn); - } - - timeline_response - .open_profile - .map(AfterRouteExecution::OpenProfile) - } + TimelineRoute::Thread(id) => ui::ThreadView::new( + threads, + ndb, + note_cache, + unknown_ids, + img_cache, + id.bytes(), + textmode, + ) + .id_source(egui::Id::new(("threadscroll", col))) + .ui(ui) + .map(Into::into), TimelineRoute::Reply(id) => { let txn = if let Ok(txn) = Transaction::new(ndb) { @@ -135,21 +102,24 @@ 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()); - let response = egui::ScrollArea::vertical().show(ui, |ui| { - ui::PostReplyView::new(ndb, poster, draft, note_cache, img_cache, &note) - .id_source(id) - .show(ui) - }); + let action = { + let draft = drafts.reply_mut(note.id()); - if let Some(action) = &response.inner.action { - PostAction::execute(poster, action, pool, draft, |np, seckey| { - np.to_reply(seckey, &note) + let response = egui::ScrollArea::vertical().show(ui, |ui| { + ui::PostReplyView::new(ndb, poster, draft, note_cache, img_cache, &note) + .id_source(id) + .show(ui) }); - } - Some(AfterRouteExecution::post(response.inner)) + response.inner.action + }; + + action.map(Into::into) + } + + TimelineRoute::Profile(pubkey) => { + render_profile_route(&pubkey, ndb, profiles, img_cache, note_cache, col, ui) } TimelineRoute::Quote(id) => { @@ -173,12 +143,7 @@ pub fn render_timeline_route( .show(ui) }); - if let Some(action) = &response.inner.action { - PostAction::execute(poster, action, pool, draft, |np, seckey| { - np.to_quote(seckey, &note) - }); - } - Some(AfterRouteExecution::post(response.inner)) + response.inner.action.map(Into::into) } } } @@ -187,16 +152,13 @@ pub fn render_timeline_route( pub fn render_profile_route( pubkey: &Pubkey, ndb: &Ndb, - columns: &mut Columns, profiles: &mut NotesHolderStorage<Profile>, - pool: &mut RelayPool, img_cache: &mut ImageCache, note_cache: &mut NoteCache, - threads: &mut NotesHolderStorage<Thread>, col: usize, ui: &mut egui::Ui, -) -> Option<AfterRouteExecution> { - let timeline_response = ProfileView::new( +) -> Option<RenderNavAction> { + let note_action = ProfileView::new( pubkey, col, profiles, @@ -206,15 +168,6 @@ pub fn render_profile_route( NoteOptions::default(), ) .ui(ui); - if let Some(bar_action) = timeline_response.bar_action { - let txn = nostrdb::Transaction::new(ndb).expect("txn"); - let mut cur_column = columns.columns_mut(); - let router = cur_column[col].router_mut(); - - bar_action.execute_and_process_result(ndb, router, threads, note_cache, pool, &txn); - } - timeline_response - .open_profile - .map(AfterRouteExecution::OpenProfile) + note_action.map(RenderNavAction::NoteAction) } diff --git a/src/ui/note/contents.rs b/src/ui/note/contents.rs @@ -1,4 +1,4 @@ -use crate::actionbar::NoteActionResponse; +use crate::actionbar::NoteAction; use crate::images::ImageType; use crate::imgcache::ImageCache; use crate::notecache::NoteCache; @@ -17,7 +17,7 @@ pub struct NoteContents<'a> { note: &'a Note<'a>, note_key: NoteKey, options: NoteOptions, - action: NoteActionResponse, + action: Option<NoteAction>, } impl<'a> NoteContents<'a> { @@ -38,11 +38,11 @@ impl<'a> NoteContents<'a> { note, note_key, options, - action: NoteActionResponse::default(), + action: None, } } - pub fn action(&self) -> &NoteActionResponse { + pub fn action(&self) -> &Option<NoteAction> { &self.action } } @@ -212,7 +212,7 @@ fn render_note_contents( let note_action = if let Some((id, block_str)) = inline_note { render_note_preview(ui, ndb, note_cache, img_cache, txn, id, block_str).action } else { - NoteActionResponse::default() + None }; if !images.is_empty() && !options.has_textmode() { diff --git a/src/ui/note/mod.rs b/src/ui/note/mod.rs @@ -8,12 +8,12 @@ pub mod reply; pub use contents::NoteContents; pub use context::{NoteContextButton, NoteContextSelection}; pub use options::NoteOptions; -pub use post::{PostAction, PostResponse, PostView}; +pub use post::{PostAction, PostResponse, PostType, PostView}; pub use quote_repost::QuoteRepostView; pub use reply::PostReplyView; use crate::{ - actionbar::{BarAction, NoteActionResponse}, + actionbar::NoteAction, app_style::NotedeckTextStyle, colors, imgcache::ImageCache, @@ -38,7 +38,7 @@ pub struct NoteView<'a> { pub struct NoteResponse { pub response: egui::Response, pub context_selection: Option<NoteContextSelection>, - pub action: NoteActionResponse, + pub action: Option<NoteAction>, } impl NoteResponse { @@ -46,11 +46,11 @@ impl NoteResponse { Self { response, context_selection: None, - action: NoteActionResponse::default(), + action: None, } } - pub fn with_action(mut self, action: NoteActionResponse) -> Self { + pub fn with_action(mut self, action: Option<NoteAction>) -> Self { self.action = action; self } @@ -437,8 +437,7 @@ impl<'a> NoteView<'a> { let note_key = self.note.key().expect("todo: support non-db notes"); let txn = self.note.txn().expect("todo: support non-db notes"); - let mut open_profile: Option<Pubkey> = None; - let mut bar_action: Option<BarAction> = None; + let mut note_action: Option<NoteAction> = None; let mut selected_option: Option<NoteContextSelection> = None; let profile = self.ndb.get_profile_by_pubkey(txn, self.note.pubkey()); @@ -454,7 +453,7 @@ impl<'a> NoteView<'a> { let response = if self.options().has_wide() { ui.horizontal(|ui| { if self.pfp(note_key, &profile, ui).clicked() { - open_profile = Some(Pubkey::new(*self.note.pubkey())); + note_action = Some(NoteAction::OpenProfile(Pubkey::new(*self.note.pubkey()))); }; let size = ui.available_size(); @@ -498,12 +497,15 @@ impl<'a> NoteView<'a> { self.options(), ); let resp = ui.add(&mut contents); - bar_action = bar_action.or(contents.action().bar_action); - open_profile = open_profile.or(contents.action().open_profile); + + if let Some(action) = contents.action() { + note_action = Some(*action); + } if self.options().has_actionbar() { - let ab = render_note_actionbar(ui, self.note.id(), note_key); - bar_action = bar_action.or(ab.inner); + if let Some(action) = render_note_actionbar(ui, self.note.id(), note_key).inner { + note_action = Some(action); + } } resp @@ -511,7 +513,7 @@ impl<'a> NoteView<'a> { // main design ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| { if self.pfp(note_key, &profile, ui).clicked() { - open_profile = Some(Pubkey::new(*self.note.pubkey())); + note_action = Some(NoteAction::OpenProfile(Pubkey::new(*self.note.pubkey()))); }; ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { @@ -548,32 +550,34 @@ impl<'a> NoteView<'a> { self.options(), ); ui.add(&mut contents); - bar_action = bar_action.or(contents.action().bar_action); - open_profile = open_profile.or(contents.action().open_profile); + + if let Some(action) = contents.action() { + note_action = Some(*action); + } if self.options().has_actionbar() { - let ab = render_note_actionbar(ui, self.note.id(), note_key); - bar_action = bar_action.or(ab.inner); + if let Some(action) = + render_note_actionbar(ui, self.note.id(), note_key).inner + { + note_action = Some(action); + } } }); }) .response }; - bar_action = check_note_hitbox( + note_action = check_note_hitbox( ui, self.note.id(), note_key, &response, maybe_hitbox, - bar_action, + note_action, ); NoteResponse::new(response) - .with_action(NoteActionResponse { - bar_action, - open_profile, - }) + .with_action(note_action) .select_option(selected_option) } } @@ -630,8 +634,8 @@ fn check_note_hitbox( note_key: NoteKey, note_response: &Response, maybe_hitbox: Option<Response>, - prior_action: Option<BarAction>, -) -> Option<BarAction> { + prior_action: Option<NoteAction>, +) -> Option<NoteAction> { // Stash the dimensions of the note content so we can render the // hitbox in the next frame ui.ctx().data_mut(|d| { @@ -640,7 +644,7 @@ fn check_note_hitbox( // If there was an hitbox and it was clicked open the thread match maybe_hitbox { - Some(hitbox) if hitbox.clicked() => Some(BarAction::OpenThread(NoteId::new(*note_id))), + Some(hitbox) if hitbox.clicked() => Some(NoteAction::OpenThread(NoteId::new(*note_id))), _ => prior_action, } } @@ -649,15 +653,15 @@ fn render_note_actionbar( ui: &mut egui::Ui, note_id: &[u8; 32], note_key: NoteKey, -) -> egui::InnerResponse<Option<BarAction>> { +) -> egui::InnerResponse<Option<NoteAction>> { 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))) + Some(NoteAction::Reply(NoteId::new(*note_id))) } else if quote_resp.clicked() { - Some(BarAction::Quote(NoteId::new(*note_id))) + Some(NoteAction::Quote(NoteId::new(*note_id))) } else { None } diff --git a/src/ui/note/post.rs b/src/ui/note/post.rs @@ -1,13 +1,14 @@ -use crate::draft::{Draft, DraftSource}; +use crate::draft::{Draft, Drafts}; use crate::imgcache::ImageCache; use crate::notecache::NoteCache; use crate::post::NewPost; use crate::ui; use crate::ui::{Preview, PreviewConfig, View}; +use crate::Result; use egui::widgets::text_edit::TextEdit; use egui::{Frame, Layout}; -use enostr::{FilledKeypair, FullKeypair, RelayPool}; -use nostrdb::{Config, Ndb, Note, Transaction}; +use enostr::{FilledKeypair, FullKeypair, NoteId, RelayPool}; +use nostrdb::{Config, Ndb, Transaction}; use tracing::info; use super::contents::render_note_preview; @@ -15,35 +16,59 @@ use super::contents::render_note_preview; pub struct PostView<'a> { ndb: &'a Ndb, draft: &'a mut Draft, - draft_source: DraftSource<'a>, + post_type: PostType, img_cache: &'a mut ImageCache, note_cache: &'a mut NoteCache, poster: FilledKeypair<'a>, id_source: Option<egui::Id>, } -pub enum PostAction { - Post(NewPost), +#[derive(Clone)] +pub enum PostType { + New, + Quote(NoteId), + Reply(NoteId), +} + +pub struct PostAction { + post_type: PostType, + post: NewPost, } impl PostAction { - pub fn execute<'b>( - poster: FilledKeypair<'_>, - action: &'b PostAction, + pub fn new(post_type: PostType, post: NewPost) -> Self { + PostAction { post_type, post } + } + + pub fn execute( + &self, + ndb: &Ndb, + txn: &Transaction, pool: &mut RelayPool, - draft: &mut Draft, - get_note: impl Fn(&'b NewPost, &[u8; 32]) -> Note<'b>, - ) { - 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)); - draft.clear(); + drafts: &mut Drafts, + ) -> Result<()> { + let seckey = self.post.account.secret_key.to_secret_bytes(); + + let note = match self.post_type { + PostType::New => self.post.to_note(&seckey), + + PostType::Reply(target) => { + let replying_to = ndb.get_note_by_id(txn, target.bytes())?; + self.post.to_reply(&seckey, &replying_to) } - } + + PostType::Quote(target) => { + let quoting = ndb.get_note_by_id(txn, target.bytes())?; + self.post.to_quote(&seckey, &quoting) + } + }; + + let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap()); + info!("sending {}", raw_msg); + pool.send(&enostr::ClientMessage::raw(raw_msg)); + drafts.get_from_post_type(&self.post_type).clear(); + + Ok(()) } } @@ -56,7 +81,7 @@ impl<'a> PostView<'a> { pub fn new( ndb: &'a Ndb, draft: &'a mut Draft, - draft_source: DraftSource<'a>, + post_type: PostType, img_cache: &'a mut ImageCache, note_cache: &'a mut NoteCache, poster: FilledKeypair<'a>, @@ -69,7 +94,7 @@ impl<'a> PostView<'a> { note_cache, poster, id_source, - draft_source, + post_type, } } @@ -162,7 +187,7 @@ impl<'a> PostView<'a> { let action = ui .horizontal(|ui| { - if let DraftSource::Quote(id) = self.draft_source { + if let PostType::Quote(id) = self.post_type { let avail_size = ui.available_size_before_wrap(); ui.with_layout(Layout::left_to_right(egui::Align::TOP), |ui| { Frame::none().show(ui, |ui| { @@ -174,7 +199,7 @@ impl<'a> PostView<'a> { self.note_cache, self.img_cache, txn, - id, + id.bytes(), "", ); }); @@ -187,10 +212,11 @@ impl<'a> PostView<'a> { .add_sized([91.0, 32.0], egui::Button::new("Post now")) .clicked() { - Some(PostAction::Post(NewPost::new( + let new_post = NewPost::new( self.draft.buffer.clone(), self.poster.to_full(), - ))) + ); + Some(PostAction::new(self.post_type.clone(), new_post)) } else { None } @@ -241,7 +267,7 @@ mod preview { PostView::new( &self.ndb, &mut self.draft, - DraftSource::Compose, + PostType::New, &mut self.img_cache, &mut self.note_cache, self.poster.to_filled(), diff --git a/src/ui/note/quote_repost.rs b/src/ui/note/quote_repost.rs @@ -1,9 +1,9 @@ -use enostr::FilledKeypair; +use enostr::{FilledKeypair, NoteId}; use nostrdb::Ndb; use crate::{draft::Draft, imgcache::ImageCache, notecache::NoteCache, ui}; -use super::PostResponse; +use super::{PostResponse, PostType}; pub struct QuoteRepostView<'a> { ndb: &'a Ndb, @@ -43,7 +43,7 @@ impl<'a> QuoteRepostView<'a> { ui::PostView::new( self.ndb, self.draft, - crate::draft::DraftSource::Quote(quoting_note_id), + PostType::Quote(NoteId::new(quoting_note_id.to_owned())), self.img_cache, self.note_cache, self.poster, diff --git a/src/ui/note/reply.rs b/src/ui/note/reply.rs @@ -2,8 +2,8 @@ use crate::draft::Draft; use crate::imgcache::ImageCache; use crate::notecache::NoteCache; use crate::ui; -use crate::ui::note::PostResponse; -use enostr::FilledKeypair; +use crate::ui::note::{PostResponse, PostType}; +use enostr::{FilledKeypair, NoteId}; use nostrdb::Ndb; pub struct PostReplyView<'a> { @@ -79,7 +79,7 @@ impl<'a> PostReplyView<'a> { ui::PostView::new( self.ndb, self.draft, - crate::draft::DraftSource::Reply(replying_to), + PostType::Reply(NoteId::new(*replying_to)), self.img_cache, self.note_cache, self.poster, diff --git a/src/ui/profile/mod.rs b/src/ui/profile/mod.rs @@ -9,7 +9,7 @@ pub use picture::ProfilePic; pub use preview::ProfilePreview; use crate::{ - actionbar::NoteActionResponse, imgcache::ImageCache, notecache::NoteCache, + actionbar::NoteAction, imgcache::ImageCache, notecache::NoteCache, notes_holder::NotesHolderStorage, profile::Profile, }; @@ -46,7 +46,7 @@ impl<'a> ProfileView<'a> { } } - pub fn ui(&mut self, ui: &mut egui::Ui) -> NoteActionResponse { + pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> { let scroll_id = egui::Id::new(("profile_scroll", self.col_id, self.pubkey)); ScrollArea::vertical() diff --git a/src/ui/thread.rs b/src/ui/thread.rs @@ -1,5 +1,5 @@ use crate::{ - actionbar::NoteActionResponse, + actionbar::NoteAction, imgcache::ImageCache, notecache::NoteCache, notes_holder::{NotesHolder, NotesHolderStorage}, @@ -52,7 +52,7 @@ impl<'a> ThreadView<'a> { self } - pub fn ui(&mut self, ui: &mut egui::Ui) -> NoteActionResponse { + pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> { let txn = Transaction::new(self.ndb).expect("txn"); let selected_note_key = if let Ok(key) = self @@ -63,7 +63,7 @@ impl<'a> ThreadView<'a> { key } else { // TODO: render 404 ? - return NoteActionResponse::default(); + return None; }; ui.label( @@ -80,7 +80,7 @@ impl<'a> ThreadView<'a> { let note = if let Ok(note) = self.ndb.get_note_by_key(&txn, selected_note_key) { note } else { - return NoteActionResponse::default(); + return None; }; let root_id = { diff --git a/src/ui/timeline.rs b/src/ui/timeline.rs @@ -1,4 +1,4 @@ -use crate::actionbar::{BarAction, NoteActionResponse}; +use crate::actionbar::NoteAction; use crate::timeline::TimelineTab; use crate::{ column::Columns, imgcache::ImageCache, notecache::NoteCache, timeline::TimelineId, ui, @@ -41,7 +41,7 @@ impl<'a> TimelineView<'a> { } } - pub fn ui(&mut self, ui: &mut egui::Ui) -> NoteActionResponse { + pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> { timeline_ui( ui, self.ndb, @@ -70,7 +70,7 @@ fn timeline_ui( img_cache: &mut ImageCache, reversed: bool, note_options: NoteOptions, -) -> NoteActionResponse { +) -> Option<NoteAction> { //padding(4.0, ui, |ui| ui.heading("Notifications")); /* let font_id = egui::TextStyle::Body.resolve(ui.style()); @@ -85,7 +85,7 @@ fn timeline_ui( error!("tried to render timeline in column, but timeline was missing"); // TODO (jb55): render error when timeline is missing? // this shouldn't happen... - return NoteActionResponse::default(); + return None; }; timeline.selected_view = tabs_ui(ui); @@ -108,7 +108,7 @@ fn timeline_ui( error!("tried to render timeline in column, but timeline was missing"); // TODO (jb55): render error when timeline is missing? // this shouldn't happen... - return NoteActionResponse::default(); + return None; }; let txn = Transaction::new(ndb).expect("failed to create txn"); @@ -241,9 +241,8 @@ impl<'a> TimelineTabView<'a> { } } - pub fn show(&mut self, ui: &mut egui::Ui) -> NoteActionResponse { - let mut open_profile = None; - let mut bar_action: Option<BarAction> = None; + pub fn show(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> { + let mut action: Option<NoteAction> = None; let len = self.tab.notes.len(); self.tab @@ -274,8 +273,9 @@ impl<'a> TimelineTabView<'a> { .note_options(self.note_options) .show(ui); - bar_action = bar_action.or(resp.action.bar_action); - open_profile = open_profile.or(resp.action.open_profile); + if let Some(note_action) = resp.action { + action = Some(note_action) + } if let Some(context) = resp.context_selection { context.process(ui, &note); @@ -288,9 +288,6 @@ impl<'a> TimelineTabView<'a> { 1 }); - NoteActionResponse { - open_profile, - bar_action, - } + action } }