notedeck

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

commit 36c0971fd95bdab33ab1e410c6627a565034a4f1
parent b4a8cddc48adb348fe103f2ddf87197e1c91f17a
Author: William Casarin <jb55@jb55.com>
Date:   Wed, 11 Sep 2024 19:43:41 -0700

Flexible routing

Another massive refactor to change the way routing works. Now any
column can route anywhere.

Also things are generally just much better and more modular via the
new struct split borrowing technique.

I didn't even try to split this into smaller commits for my sanity.

Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
M.envrc | 1+
Msrc/account_manager.rs | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Msrc/actionbar.rs | 61+++++++++++++++++++++++++++++++++++++++++--------------------
Msrc/app.rs | 386++++++++++++++++++++++++-------------------------------------------------------
Msrc/column.rs | 137+++++++++++++++++++++++--------------------------------------------------------
Msrc/error.rs | 4+++-
Msrc/lib.rs | 3++-
Msrc/login_manager.rs | 8+-------
Asrc/nav.rs | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/note.rs | 8+++++++-
Dsrc/routable_widget_state.rs | 26--------------------------
Msrc/route.rs | 118++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/test_data.rs | 4++--
Msrc/thread.rs | 127++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/timeline/mod.rs | 221++++++++++++++++++++++++++++---------------------------------------------------
Asrc/timeline/route.rs | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/ui/account_management.rs | 97+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/ui/account_switcher.rs | 16+++++++++-------
Msrc/ui/mod.rs | 3+--
Msrc/ui/note/mod.rs | 10++++++----
Msrc/ui/profile/profile_preview_controller.rs | 28++++++++++++++--------------
Dsrc/ui/stateful_account_management.rs | 131-------------------------------------------------------------------------------
Msrc/ui/thread.rs | 101+++++++++++++++++++++++++++----------------------------------------------------
Msrc/ui/timeline.rs | 88+++++++++++++++++++++++++++++++------------------------------------------------
Msrc/ui_preview/main.rs | 10++++------
Msrc/unknowns.rs | 41+++++++++++++++++++++++++++++++++++++----
Asrc/view_state.rs | 13+++++++++++++
27 files changed, 972 insertions(+), 962 deletions(-)

diff --git a/.envrc b/.envrc @@ -12,3 +12,4 @@ export JB55=32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245 export JACK=npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m export VROD=npub1h50pnxqw9jg7dhr906fvy4mze2yzawf895jhnc3p7qmljdugm6gsrurqev +export JEFFG=npub1zuuajd7u3sx8xu92yav9jwxpr839cs0kc3q6t56vd5u9q033xmhsk6c2uc diff --git a/src/account_manager.rs b/src/account_manager.rs @@ -1,16 +1,23 @@ use std::cmp::Ordering; use enostr::{FilledKeypair, FullKeypair, Keypair}; +use nostrdb::Ndb; -pub use crate::user_account::UserAccount; use crate::{ + column::Columns, + imgcache::ImageCache, key_storage::{KeyStorage, KeyStorageResponse, KeyStorageType}, + login_manager::LoginState, + route::{Route, Router}, ui::{ - account_login_view::AccountLoginResponse, account_management::AccountManagementViewResponse, + account_login_view::{AccountLoginResponse, AccountLoginView}, + account_management::{AccountsView, AccountsViewResponse}, }, }; use tracing::info; +pub use crate::user_account::UserAccount; + /// The interface for managing the user's accounts. /// Represents all user-facing operations related to account management. pub struct AccountManager { @@ -19,6 +26,75 @@ pub struct AccountManager { key_store: KeyStorageType, } +// TODO(jb55): move to accounts/route.rs +pub enum AccountsRouteResponse { + Accounts(AccountsViewResponse), + AddAccount(AccountLoginResponse), +} + +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +pub enum AccountsRoute { + Accounts, + AddAccount, +} + +/// Render account management views from a route +#[allow(clippy::too_many_arguments)] +pub fn render_accounts_route( + ui: &mut egui::Ui, + ndb: &Ndb, + col: usize, + columns: &mut Columns, + img_cache: &mut ImageCache, + accounts: &mut AccountManager, + login_state: &mut LoginState, + route: AccountsRoute, +) { + let router = columns.column_mut(col).router_mut(); + let resp = match route { + AccountsRoute::Accounts => AccountsView::new(ndb, accounts, img_cache) + .ui(ui) + .inner + .map(AccountsRouteResponse::Accounts), + + AccountsRoute::AddAccount => AccountLoginView::new(login_state) + .ui(ui) + .inner + .map(AccountsRouteResponse::AddAccount), + }; + + if let Some(resp) = resp { + match resp { + AccountsRouteResponse::Accounts(response) => { + process_accounts_view_response(accounts, response, router); + } + AccountsRouteResponse::AddAccount(response) => { + process_login_view_response(accounts, response); + *login_state = Default::default(); + router.go_back(); + } + } + } +} + +pub fn process_accounts_view_response( + manager: &mut AccountManager, + response: AccountsViewResponse, + router: &mut Router<Route>, +) { + match response { + AccountsViewResponse::RemoveAccount(index) => { + manager.remove_account(index); + } + AccountsViewResponse::SelectAccount(index) => { + manager.select_account(index); + } + AccountsViewResponse::RouteToLogin => { + router.route_to(Route::add_account()); + } + } +} + impl AccountManager { pub fn new(currently_selected_account: Option<usize>, key_store: KeyStorageType) -> Self { let accounts = if let KeyStorageResponse::ReceivedResult(res) = key_store.get_keys() { @@ -122,21 +198,6 @@ impl AccountManager { } } -pub fn process_management_view_response_stateless( - manager: &mut AccountManager, - response: AccountManagementViewResponse, -) { - match response { - AccountManagementViewResponse::RemoveAccount(index) => { - manager.remove_account(index); - } - AccountManagementViewResponse::SelectAccount(index) => { - manager.select_account(index); - } - AccountManagementViewResponse::RouteToLogin => {} - } -} - pub fn process_login_view_response(manager: &mut AccountManager, response: AccountLoginResponse) { match response { AccountLoginResponse::CreateNew => { diff --git a/src/actionbar.rs b/src/actionbar.rs @@ -1,8 +1,7 @@ use crate::{ - column::Column, note::NoteRef, notecache::NoteCache, - route::Route, + route::{Route, Router}, thread::{Thread, ThreadResult, Threads}, }; use enostr::{NoteId, RelayPool}; @@ -12,8 +11,8 @@ use uuid::Uuid; #[derive(Debug, Eq, PartialEq, Copy, Clone)] pub enum BarAction { - Reply, - OpenThread, + Reply(NoteId), + OpenThread(NoteId), } pub struct NewThreadNotes { @@ -33,17 +32,15 @@ pub enum BarResult { fn open_thread( ndb: &Ndb, txn: &Transaction, - column: &mut Column, + router: &mut Router<Route>, note_cache: &mut NoteCache, pool: &mut RelayPool, threads: &mut Threads, selected_note: &[u8; 32], ) -> Option<BarResult> { { - column - .routes_mut() - .push(Route::Thread(NoteId::new(selected_note.to_owned()))); - column.navigating = true; + router.route_to(Route::thread(NoteId::new(selected_note.to_owned()))); + router.navigating = true; } let root_id = crate::note::root_note_id_from_selected_id(ndb, note_cache, txn, selected_note); @@ -52,7 +49,7 @@ fn open_thread( let (thread, result) = match thread_res { ThreadResult::Stale(thread) => { // The thread is stale, let's update it - let notes = Thread::new_notes(&thread.view.notes, root_id, txn, ndb); + let notes = Thread::new_notes(&thread.view().notes, root_id, txn, ndb); let bar_result = if notes.is_empty() { None } else { @@ -120,33 +117,57 @@ impl BarAction { pub fn execute( self, ndb: &Ndb, - column: &mut Column, + router: &mut Router<Route>, threads: &mut Threads, note_cache: &mut NoteCache, pool: &mut RelayPool, - replying_to: &[u8; 32], txn: &Transaction, ) -> Option<BarResult> { match self { - BarAction::Reply => { - column - .routes_mut() - .push(Route::Reply(NoteId::new(replying_to.to_owned()))); - column.navigating = true; + BarAction::Reply(note_id) => { + router.route_to(Route::reply(note_id)); + router.navigating = true; None } - BarAction::OpenThread => { - open_thread(ndb, txn, column, note_cache, pool, threads, replying_to) + BarAction::OpenThread(note_id) => { + open_thread(ndb, txn, router, note_cache, pool, threads, note_id.bytes()) } } } + + /// Execute the BarAction and process the BarResult + pub fn execute_and_process_result( + self, + ndb: &Ndb, + router: &mut Router<Route>, + threads: &mut Threads, + note_cache: &mut NoteCache, + pool: &mut RelayPool, + txn: &Transaction, + ) { + if let Some(br) = self.execute(ndb, router, threads, note_cache, pool, txn) { + br.process(ndb, txn, threads); + } + } } impl BarResult { pub fn new_thread_notes(notes: Vec<NoteRef>, root_id: NoteId) -> Self { BarResult::NewThreadNotes(NewThreadNotes::new(notes, root_id)) } + + pub fn process(&self, ndb: &Ndb, txn: &Transaction, threads: &mut Threads) { + match self { + // update the thread for next render if we have new notes + BarResult::NewThreadNotes(new_notes) => { + let thread = threads + .thread_mut(ndb, txn, new_notes.root_id.bytes()) + .get_ptr(); + new_notes.process(thread); + } + } + } } impl NewThreadNotes { @@ -159,6 +180,6 @@ impl NewThreadNotes { pub fn process(&self, thread: &mut Thread) { // threads are chronological, ie reversed from reverse-chronological, the default. let reversed = true; - thread.view.insert(&self.notes, reversed); + thread.view_mut().insert(&self.notes, reversed); } } diff --git a/src/app.rs b/src/app.rs @@ -1,30 +1,28 @@ -use crate::account_manager::AccountManager; -use crate::actionbar::BarResult; -use crate::app_creation::setup_cc; -use crate::app_style::user_requested_visuals_change; -use crate::args::Args; -use crate::column::{Column, ColumnKind, Columns}; -use crate::draft::Drafts; -use crate::error::{Error, FilterError}; -use crate::filter::FilterState; -use crate::frame_history::FrameHistory; -use crate::imgcache::ImageCache; -use crate::key_storage::KeyStorageType; -use crate::login_manager::LoginState; -use crate::note::NoteRef; -use crate::notecache::{CachedNote, NoteCache}; -use crate::relay_pool_manager::RelayPoolManager; -use crate::routable_widget_state::RoutableWidgetState; -use crate::route::{ManageAccountRoute, Route}; -use crate::subscriptions::{SubKind, Subscriptions}; -use crate::thread::{DecrementResult, Threads}; -use crate::timeline::{Timeline, TimelineKind, TimelineSource, ViewFilter}; -use crate::ui::note::PostAction; -use crate::ui::{self, AccountSelectionWidget}; -use crate::ui::{DesktopSidePanel, RelayView, View}; -use crate::unknowns::UnknownIds; -use crate::{filter, Result}; -use egui_nav::{Nav, NavAction}; +use crate::{ + account_manager::AccountManager, + app_creation::setup_cc, + app_style::user_requested_visuals_change, + args::Args, + column::Columns, + draft::Drafts, + error::{Error, FilterError}, + filter, + filter::FilterState, + frame_history::FrameHistory, + imgcache::ImageCache, + key_storage::KeyStorageType, + nav, + note::NoteRef, + notecache::{CachedNote, NoteCache}, + subscriptions::{SubKind, Subscriptions}, + thread::Threads, + timeline::{Timeline, TimelineKind, ViewFilter}, + ui::{self, AccountSelectionWidget, DesktopSidePanel}, + unknowns::UnknownIds, + view_state::ViewState, + Result, +}; + use enostr::{ClientMessage, RelayEvent, RelayMessage, RelayPool}; use uuid::Uuid; @@ -47,18 +45,17 @@ pub enum DamusState { /// We derive Deserialize/Serialize so we can persist app state on shutdown. pub struct Damus { state: DamusState, - note_cache: NoteCache, + pub note_cache: NoteCache, pub pool: RelayPool, pub columns: Columns, - pub account_management_view_state: RoutableWidgetState<ManageAccountRoute>, pub ndb: Ndb, + pub view_state: ViewState, pub unknown_ids: UnknownIds, pub drafts: Drafts, pub threads: Threads, pub img_cache: ImageCache, pub accounts: AccountManager, - pub login_state: LoginState, pub subscriptions: Subscriptions, frame_history: crate::frame_history::FrameHistory, @@ -267,24 +264,24 @@ fn try_process_event(damus: &mut Damus, ctx: &egui::Context) -> Result<()> { } } - let n_cols = damus.columns.columns().len(); - for col_ind in 0..n_cols { - let timeline = - if let ColumnKind::Timeline(timeline) = damus.columns.column_mut(col_ind).kind_mut() { - timeline - } else { - continue; - }; + let n_timelines = damus.columns.timelines().len(); + for timeline_ind in 0..n_timelines { + let is_ready = { + let timeline = &mut damus.columns.timelines[timeline_ind]; + matches!( + is_timeline_ready(&damus.ndb, &mut damus.pool, &mut damus.note_cache, timeline), + Ok(true) + ) + }; - if let Ok(true) = - is_timeline_ready(&damus.ndb, &mut damus.pool, &mut damus.note_cache, timeline) - { + if is_ready { let txn = Transaction::new(&damus.ndb).expect("txn"); - if let Err(err) = TimelineSource::column(timeline.id).poll_notes_into_view( - &txn, + + if let Err(err) = Timeline::poll_notes_into_view( + timeline_ind, + &mut damus.columns.timelines, &damus.ndb, - &mut damus.columns, - &mut damus.threads, + &txn, &mut damus.unknown_ids, &mut damus.note_cache, ) { @@ -667,21 +664,21 @@ impl Damus { .map(|a| a.pubkey.bytes()); let ndb = Ndb::new(&dbpath, &config).expect("ndb"); - let mut columns: Vec<Column> = Vec::with_capacity(parsed_args.columns.len()); + let mut columns: Columns = Columns::new(); for col in parsed_args.columns { if let Some(timeline) = col.into_timeline(&ndb, account) { - columns.push(Column::timeline(timeline)); + columns.add_timeline(timeline); } } let debug = parsed_args.debug; - if columns.is_empty() { + if columns.columns().is_empty() { let filter = Filter::from_json(include_str!("../queries/timeline.json")).unwrap(); - columns.push(Column::timeline(Timeline::new( + columns.add_timeline(Timeline::new( TimelineKind::Generic, FilterState::ready(vec![filter]), - ))); + )) } Self { @@ -695,17 +692,52 @@ impl Damus { state: DamusState::Initializing, img_cache: ImageCache::new(imgcache_dir.into()), note_cache: NoteCache::default(), - columns: Columns::new(columns), + columns, textmode: parsed_args.textmode, ndb, accounts, frame_history: FrameHistory::default(), show_account_switcher: false, - account_management_view_state: RoutableWidgetState::default(), - login_state: LoginState::default(), + view_state: ViewState::default(), } } + pub fn pool_mut(&mut self) -> &mut RelayPool { + &mut self.pool + } + + pub fn ndb(&self) -> &Ndb { + &self.ndb + } + + pub fn drafts_mut(&mut self) -> &mut Drafts { + &mut self.drafts + } + + pub fn img_cache_mut(&mut self) -> &mut ImageCache { + &mut self.img_cache + } + + pub fn accounts(&self) -> &AccountManager { + &self.accounts + } + + pub fn accounts_mut(&mut self) -> &mut AccountManager { + &mut self.accounts + } + + pub fn view_state_mut(&mut self) -> &mut ViewState { + &mut self.view_state + } + + pub fn columns_mut(&mut self) -> &mut Columns { + &mut self.columns + } + + pub fn columns(&self) -> &Columns { + &self.columns + } + pub fn gen_subid(&self, kind: &SubKind) -> String { if self.debug { format!("{:?}", kind) @@ -715,12 +747,12 @@ impl Damus { } pub fn mock<P: AsRef<Path>>(data_path: P) -> Self { - let mut columns: Vec<Column> = vec![]; + let mut columns = Columns::new(); let filter = Filter::from_json(include_str!("../queries/global.json")).unwrap(); - columns.push(Column::timeline(Timeline::new( - TimelineKind::Universe, - FilterState::ready(vec![filter]), - ))); + + let timeline = Timeline::new(TimelineKind::Universe, FilterState::ready(vec![filter])); + + columns.add_timeline(timeline); let imgcache_dir = data_path.as_ref().join(ImageCache::rel_datadir()); let _ = std::fs::create_dir_all(imgcache_dir.clone()); @@ -739,14 +771,13 @@ impl Damus { pool: RelayPool::new(), img_cache: ImageCache::new(imgcache_dir), note_cache: NoteCache::default(), - columns: Columns::new(columns), + columns, textmode: false, ndb: Ndb::new(data_path.as_ref().to_str().expect("db path ok"), &config).expect("ndb"), accounts: AccountManager::new(None, KeyStorageType::None), frame_history: FrameHistory::default(), show_account_switcher: false, - account_management_view_state: RoutableWidgetState::default(), - login_state: LoginState::default(), + view_state: ViewState::default(), } } @@ -758,6 +789,18 @@ impl Damus { &mut self.note_cache } + pub fn unknown_ids_mut(&mut self) -> &mut UnknownIds { + &mut self.unknown_ids + } + + pub fn threads(&self) -> &Threads { + &self.threads + } + + pub fn threads_mut(&mut self) -> &mut Threads { + &mut self.threads + } + pub fn note_cache(&self) -> &NoteCache { &self.note_cache } @@ -852,211 +895,6 @@ fn render_panel(ctx: &egui::Context, app: &mut Damus) { }); } -/// Local thread unsubscribe -fn thread_unsubscribe( - ndb: &Ndb, - threads: &mut Threads, - pool: &mut RelayPool, - note_cache: &mut NoteCache, - id: &[u8; 32], -) { - let (unsubscribe, remote_subid) = { - let txn = Transaction::new(ndb).expect("txn"); - let root_id = crate::note::root_note_id_from_selected_id(ndb, note_cache, &txn, id); - - let thread = threads.thread_mut(ndb, &txn, root_id).get_ptr(); - let unsub = thread.decrement_sub(); - - let mut remote_subid: Option<String> = None; - if let Ok(DecrementResult::LastSubscriber(_subid)) = unsub { - *thread.subscription_mut() = None; - remote_subid = thread.remote_subscription().to_owned(); - *thread.remote_subscription_mut() = None; - } - - (unsub, remote_subid) - }; - - match unsubscribe { - Ok(DecrementResult::LastSubscriber(sub)) => { - if let Err(e) = ndb.unsubscribe(sub) { - error!( - "failed to unsubscribe from thread: {e}, subid:{}, {} active subscriptions", - sub.id(), - ndb.subscription_count() - ); - } else { - info!( - "Unsubscribed from thread subid:{}. {} active subscriptions", - sub.id(), - ndb.subscription_count() - ); - } - - // unsub from remote - if let Some(subid) = remote_subid { - pool.unsubscribe(subid); - } - } - - Ok(DecrementResult::ActiveSubscribers) => { - info!( - "Keeping thread subscription. {} active subscriptions.", - ndb.subscription_count() - ); - // do nothing - } - - Err(e) => { - // something is wrong! - error!( - "Thread unsubscribe error: {e}. {} active subsciptions.", - ndb.subscription_count() - ); - } - } -} - -fn render_nav(show_postbox: bool, col: usize, app: &mut Damus, ui: &mut egui::Ui) { - let navigating = app.columns.column(col).navigating; - let returning = app.columns.column(col).returning; - - let nav_response = Nav::new(app.columns.column(col).routes().to_vec()) - .navigating(navigating) - .returning(returning) - .title(false) - .show_mut(ui, |ui, nav| match nav.top() { - Route::Timeline(_n) => { - let column = app.columns.column_mut(col); - if column.kind().timeline().is_some() { - if show_postbox { - if let Some(kp) = app.accounts.selected_or_first_nsec() { - ui::timeline::postbox_view( - &app.ndb, - kp, - &mut app.pool, - &mut app.drafts, - &mut app.img_cache, - ui, - ); - } - } - ui::TimelineView::new( - &app.ndb, - column, - &mut app.note_cache, - &mut app.img_cache, - &mut app.threads, - &mut app.pool, - app.textmode, - ) - .ui(ui); - } else { - ui.label("no timeline for this column?"); - } - None - } - - Route::Relays => { - let manager = RelayPoolManager::new(&mut app.pool); - RelayView::new(manager).ui(ui); - None - } - - Route::Thread(id) => { - let result = ui::ThreadView::new( - col, - &mut app.columns, - &mut app.threads, - &app.ndb, - &mut app.note_cache, - &mut app.img_cache, - &mut app.unknown_ids, - &mut app.pool, - app.textmode, - id.bytes(), - ) - .ui(ui); - - if let Some(bar_result) = result { - match bar_result { - BarResult::NewThreadNotes(new_notes) => { - let thread = app.threads.thread_expected_mut(new_notes.root_id.bytes()); - new_notes.process(thread); - } - } - } - - None - } - - Route::Reply(id) => { - let txn = if let Ok(txn) = Transaction::new(&app.ndb) { - txn - } else { - ui.label("Reply to unknown note"); - return None; - }; - - let note = if let Ok(note) = app.ndb.get_note_by_id(&txn, id.bytes()) { - note - } else { - ui.label("Reply to unknown note"); - return None; - }; - - let id = egui::Id::new(( - "post", - app.columns.column(col).view_id(), - note.key().unwrap(), - )); - - if let Some(poster) = app.accounts.selected_or_first_nsec() { - let response = egui::ScrollArea::vertical().show(ui, |ui| { - ui::PostReplyView::new( - &app.ndb, - poster, - &mut app.pool, - &mut app.drafts, - &mut app.note_cache, - &mut app.img_cache, - &note, - ) - .id_source(id) - .show(ui) - }); - - Some(response) - } else { - None - } - } - }); - - let column = app.columns.column_mut(col); - if let Some(reply_response) = nav_response.inner { - if let Some(PostAction::Post(_np)) = reply_response.inner.action { - column.returning = true; - } - } - - if let Some(NavAction::Returned) = nav_response.action { - let popped = column.routes_mut().pop(); - if let Some(Route::Thread(id)) = popped { - thread_unsubscribe( - &app.ndb, - &mut app.threads, - &mut app.pool, - &mut app.note_cache, - id.bytes(), - ); - } - column.returning = false; - } else if let Some(NavAction::Navigated) = nav_response.action { - column.navigating = false; - } -} - fn render_damus_mobile(ctx: &egui::Context, app: &mut Damus) { //render_panel(ctx, app, 0); @@ -1067,7 +905,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() { - render_nav(false, 0, app, ui); + nav::render_nav(false, 0, app, ui); } }); } @@ -1143,12 +981,20 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, columns: usiz for column_ind in 0..n_cols { strip.cell(|ui| { let rect = ui.available_rect_before_wrap(); - let show_postbox = - first && app.columns.column(column_ind).kind().timeline().is_some(); + let show_postbox = first + && app + .columns + .column(column_ind) + .router() + .routes() + .iter() + .find_map(|r| r.timeline_id()) + .is_some(); if show_postbox { first = false } - render_nav(show_postbox, column_ind, app, ui); + + nav::render_nav(show_postbox, column_ind, app, ui); // vertical line ui.painter().vline( diff --git a/src/column.rs b/src/column.rs @@ -1,100 +1,86 @@ -use crate::route::Route; +use crate::route::{Route, Router}; use crate::timeline::{Timeline, TimelineId}; use std::iter::Iterator; use tracing::warn; pub struct Column { - kind: ColumnKind, - routes: Vec<Route>, - - pub navigating: bool, - pub returning: bool, + router: Router<Route>, } impl Column { - pub fn timeline(timeline: Timeline) -> Self { - let routes = vec![Route::Timeline(format!("{}", &timeline.kind))]; - let kind = ColumnKind::Timeline(timeline); - Column::new(kind, routes) - } - - pub fn kind(&self) -> &ColumnKind { - &self.kind - } - - pub fn kind_mut(&mut self) -> &mut ColumnKind { - &mut self.kind + pub fn new(routes: Vec<Route>) -> Self { + let router = Router::new(routes); + Column { router } } - pub fn view_id(&self) -> egui::Id { - self.kind.view_id() + pub fn router(&self) -> &Router<Route> { + &self.router } - pub fn routes(&self) -> &[Route] { - &self.routes - } - - pub fn routes_mut(&mut self) -> &mut Vec<Route> { - &mut self.routes - } - - pub fn new(kind: ColumnKind, routes: Vec<Route>) -> Self { - let navigating = false; - let returning = false; - Column { - kind, - routes, - navigating, - returning, - } + pub fn router_mut(&mut self) -> &mut Router<Route> { + &mut self.router } } +#[derive(Default)] pub struct Columns { + /// Columns are simply routers into settings, timelines, etc columns: Vec<Column>, + /// Timeline state is not tied to routing logic separately, so that + /// different columns can navigate to and from settings to timelines, + /// etc. + pub timelines: Vec<Timeline>, + /// The selected column for key navigation selected: i32, } impl Columns { + pub fn new() -> Self { + Columns::default() + } + + pub fn add_timeline(&mut self, timeline: Timeline) { + let routes = vec![Route::timeline(timeline.id)]; + self.timelines.push(timeline); + self.columns.push(Column::new(routes)) + } + pub fn columns_mut(&mut self) -> &mut Vec<Column> { &mut self.columns } + pub fn timeline_mut(&mut self, timeline_ind: usize) -> &mut Timeline { + &mut self.timelines[timeline_ind] + } + pub fn column(&self, ind: usize) -> &Column { &self.columns()[ind] } - pub fn columns(&self) -> &[Column] { + pub fn columns(&self) -> &Vec<Column> { &self.columns } - pub fn new(columns: Vec<Column>) -> Self { - let selected = 0; - Columns { columns, selected } - } - pub fn selected(&mut self) -> &mut Column { &mut self.columns[self.selected as usize] } - pub fn timelines_mut(&mut self) -> impl Iterator<Item = &mut Timeline> { - self.columns - .iter_mut() - .filter_map(|c| c.kind_mut().timeline_mut()) + pub fn timelines_mut(&mut self) -> &mut Vec<Timeline> { + &mut self.timelines } - pub fn timelines(&self) -> impl Iterator<Item = &Timeline> { - self.columns.iter().filter_map(|c| c.kind().timeline()) + pub fn timelines(&self) -> &Vec<Timeline> { + &self.timelines } pub fn find_timeline_mut(&mut self, id: TimelineId) -> Option<&mut Timeline> { - self.timelines_mut().find(|tl| tl.id == id) + self.timelines_mut().iter_mut().find(|tl| tl.id == id) } pub fn find_timeline(&self, id: TimelineId) -> Option<&Timeline> { - self.timelines().find(|tl| tl.id == id) + self.timelines().iter().find(|tl| tl.id == id) } pub fn column_mut(&mut self, ind: usize) -> &mut Column { @@ -102,11 +88,11 @@ impl Columns { } pub fn select_down(&mut self) { - self.selected().kind_mut().select_down(); + warn!("todo: implement select_down"); } pub fn select_up(&mut self) { - self.selected().kind_mut().select_up(); + warn!("todo: implement select_up"); } pub fn select_left(&mut self) { @@ -123,48 +109,3 @@ impl Columns { self.selected += 1; } } - -/// What type of column is it? -#[derive(Debug)] -pub enum ColumnKind { - Timeline(Timeline), - - ManageAccount, -} - -impl ColumnKind { - pub fn timeline_mut(&mut self) -> Option<&mut Timeline> { - match self { - ColumnKind::Timeline(tl) => Some(tl), - _ => None, - } - } - - pub fn timeline(&self) -> Option<&Timeline> { - match self { - ColumnKind::Timeline(tl) => Some(tl), - _ => None, - } - } - - pub fn view_id(&self) -> egui::Id { - match self { - ColumnKind::Timeline(timeline) => timeline.view_id(), - ColumnKind::ManageAccount => egui::Id::new("manage_account"), - } - } - - pub fn select_down(&mut self) { - match self { - ColumnKind::Timeline(tl) => tl.current_view_mut().select_down(), - ColumnKind::ManageAccount => warn!("todo: manage account select_down"), - } - } - - pub fn select_up(&mut self) { - match self { - ColumnKind::Timeline(tl) => tl.current_view_mut().select_down(), - ColumnKind::ManageAccount => warn!("todo: manage account select_down"), - } - } -} diff --git a/src/error.rs b/src/error.rs @@ -40,9 +40,10 @@ impl fmt::Display for SubscriptionError { #[derive(Debug)] pub enum Error { + TimelineNotFound, + LoadFailed, SubscriptionError(SubscriptionError), Filter(FilterError), - LoadFailed, Io(io::Error), Nostr(enostr::Error), Ndb(nostrdb::Error), @@ -72,6 +73,7 @@ impl fmt::Display for Error { Self::SubscriptionError(e) => { write!(f, "{e}") } + Self::TimelineNotFound => write!(f, "Timeline not found"), Self::LoadFailed => { write!(f, "load failed") } diff --git a/src/lib.rs b/src/lib.rs @@ -21,13 +21,13 @@ mod key_parsing; mod key_storage; pub mod login_manager; mod macos_key_storage; +mod nav; mod note; mod notecache; mod post; mod profile; pub mod relay_pool_manager; mod result; -mod routable_widget_state; mod route; mod subscriptions; mod test_data; @@ -38,6 +38,7 @@ mod timeline; pub mod ui; mod unknowns; mod user_account; +mod view_state; #[cfg(test)] #[macro_use] diff --git a/src/login_manager.rs b/src/login_manager.rs @@ -16,13 +16,7 @@ pub struct LoginState { impl<'a> LoginState { pub fn new() -> Self { - LoginState { - login_key: String::new(), - promise_query: None, - error: None, - key_on_error: None, - should_create_new: false, - } + LoginState::default() } /// Get the textedit for the login UI without exposing the key variable diff --git a/src/nav.rs b/src/nav.rs @@ -0,0 +1,84 @@ +use crate::{ + account_manager::render_accounts_route, + relay_pool_manager::RelayPoolManager, + route::Route, + thread::thread_unsubscribe, + timeline::route::{render_timeline_route, TimelineRoute, TimelineRouteResponse}, + ui::{note::PostAction, RelayView, View}, + Damus, +}; + +use egui_nav::{Nav, NavAction}; + +pub fn render_nav(show_postbox: bool, col: usize, app: &mut Damus, ui: &mut egui::Ui) { + let nav_response = Nav::new(app.columns().column(col).router().routes().clone()) + .navigating(app.columns_mut().column_mut(col).router_mut().navigating) + .returning(app.columns_mut().column_mut(col).router_mut().returning) + .title(false) + .show_mut(ui, |ui, nav| match nav.top() { + Route::Timeline(tlr) => render_timeline_route( + &app.ndb, + &mut app.columns, + &mut app.pool, + &mut app.drafts, + &mut app.img_cache, + &mut app.note_cache, + &mut app.threads, + &mut app.accounts, + *tlr, + col, + show_postbox, + app.textmode, + ui, + ), + Route::Accounts(amr) => { + render_accounts_route( + ui, + &app.ndb, + col, + &mut app.columns, + &mut app.img_cache, + &mut app.accounts, + &mut app.view_state.login, + *amr, + ); + None + } + Route::Relays => { + let manager = RelayPoolManager::new(app.pool_mut()); + RelayView::new(manager).ui(ui); + None + } + }); + + if let Some(reply_response) = nav_response.inner { + // start returning when we're finished posting + match reply_response { + TimelineRouteResponse::Post(resp) => { + if let Some(action) = resp.action { + match action { + PostAction::Post(_) => { + app.columns_mut().column_mut(col).router_mut().returning = true; + } + } + } + } + } + } + + if let Some(NavAction::Returned) = nav_response.action { + let r = app.columns_mut().column_mut(col).router_mut().go_back(); + if let Some(Route::Timeline(TimelineRoute::Thread(id))) = r { + thread_unsubscribe( + &app.ndb, + &mut app.threads, + &mut app.pool, + &mut app.note_cache, + id.bytes(), + ); + } + app.columns_mut().column_mut(col).router_mut().returning = false; + } else if let Some(NavAction::Navigated) = nav_response.action { + app.columns_mut().column_mut(col).router_mut().navigating = false; + } +} diff --git a/src/note.rs b/src/note.rs @@ -1,5 +1,5 @@ use crate::notecache::NoteCache; -use nostrdb::{Ndb, NoteKey, QueryResult, Transaction}; +use nostrdb::{Ndb, Note, NoteKey, QueryResult, Transaction}; use std::cmp::Ordering; #[derive(Debug, Eq, PartialEq, Copy, Clone)] @@ -13,6 +13,12 @@ impl NoteRef { NoteRef { key, created_at } } + pub fn from_note(note: &Note<'_>) -> Self { + let created_at = note.created_at(); + let key = note.key().expect("todo: implement NoteBuf"); + NoteRef::new(key, created_at) + } + pub fn from_query_result(qr: QueryResult<'_>) -> Self { NoteRef { key: qr.note_key, diff --git a/src/routable_widget_state.rs b/src/routable_widget_state.rs @@ -1,26 +0,0 @@ -#[derive(Default)] -pub struct RoutableWidgetState<R: Clone> { - routes: Vec<R>, -} - -impl<R: Clone> RoutableWidgetState<R> { - pub fn route_to(&mut self, route: R) { - self.routes.push(route); - } - - pub fn clear(&mut self) { - self.routes.clear(); - } - - pub fn go_back(&mut self) { - self.routes.pop(); - } - - pub fn top(&self) -> Option<R> { - self.routes.last().cloned() - } - - pub fn get_routes(&self) -> Vec<R> { - self.routes.clone() - } -} diff --git a/src/route.rs b/src/route.rs @@ -1,51 +1,107 @@ -use egui::RichText; use enostr::NoteId; use std::fmt::{self}; -use strum_macros::Display; -use crate::ui::{ - account_login_view::AccountLoginResponse, account_management::AccountManagementViewResponse, +use crate::{ + account_manager::AccountsRoute, + timeline::{TimelineId, TimelineRoute}, }; /// App routing. These describe different places you can go inside Notedeck. -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Eq, PartialEq, Debug)] pub enum Route { - Timeline(String), - Thread(NoteId), - Reply(NoteId), + Timeline(TimelineRoute), + Accounts(AccountsRoute), Relays, } -#[derive(Clone, Debug, Default, Display)] -pub enum ManageAccountRoute { - #[default] - AccountManagement, - AddAccount, -} +impl Route { + pub fn timeline(timeline_id: TimelineId) -> Self { + Route::Timeline(TimelineRoute::Timeline(timeline_id)) + } -impl fmt::Display for Route { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Route::Timeline(name) => write!(f, "{}", name), - Route::Thread(_id) => write!(f, "Thread"), - Route::Reply(_id) => write!(f, "Reply"), - Route::Relays => write!(f, "Relays"), + pub fn timeline_id(&self) -> Option<&TimelineId> { + if let Route::Timeline(TimelineRoute::Timeline(tid)) = self { + Some(tid) + } else { + None } } + + pub fn thread(thread_root: NoteId) -> Self { + Route::Timeline(TimelineRoute::Thread(thread_root)) + } + + pub fn reply(replying_to: NoteId) -> Self { + Route::Timeline(TimelineRoute::Reply(replying_to)) + } + + pub fn accounts() -> Self { + Route::Accounts(AccountsRoute::Accounts) + } + + pub fn add_account() -> Self { + Route::Accounts(AccountsRoute::AddAccount) + } } -impl Route { - pub fn title(&self) -> RichText { - match self { - Route::Thread(_) => RichText::new("Thread"), - Route::Reply(_) => RichText::new("Reply"), - Route::Relays => RichText::new("Relays"), - Route::Timeline(_) => RichText::new("Timeline"), +// TODO: add this to egui-nav so we don't have to deal with returning +// and navigating headaches +#[derive(Clone)] +pub struct Router<R: Clone> { + routes: Vec<R>, + pub returning: bool, + pub navigating: bool, +} + +impl<R: Clone> Router<R> { + pub fn new(routes: Vec<R>) -> Self { + if routes.is_empty() { + panic!("routes can't be empty") } + let returning = false; + let navigating = false; + Router { + routes, + returning, + navigating, + } + } + + pub fn route_to(&mut self, route: R) { + self.routes.push(route); + } + + pub fn go_back(&mut self) -> Option<R> { + if self.routes.len() == 1 { + return None; + } + self.routes.pop() + } + + pub fn top(&self) -> &R { + self.routes.last().expect("routes can't be empty") + } + + pub fn routes(&self) -> &Vec<R> { + &self.routes } } -pub enum ManageAcountRouteResponse { - AccountManagement(AccountManagementViewResponse), - AddAccount(AccountLoginResponse), +impl fmt::Display for Route { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Route::Timeline(tlr) => match tlr { + TimelineRoute::Timeline(name) => write!(f, "{}", name), + TimelineRoute::Thread(_id) => write!(f, "Thread"), + TimelineRoute::Reply(_id) => write!(f, "Reply"), + }, + + Route::Relays => write!(f, "Relays"), + + Route::Accounts(amr) => match amr { + AccountsRoute::Accounts => write!(f, "Accounts"), + AccountsRoute::AddAccount => write!(f, "Add Account"), + }, + } + } } diff --git a/src/test_data.rs b/src/test_data.rs @@ -3,7 +3,7 @@ use std::path::Path; use enostr::{FullKeypair, Pubkey, RelayPool}; use nostrdb::ProfileRecord; -use crate::{account_manager::UserAccount, Damus}; +use crate::{user_account::UserAccount, Damus}; #[allow(unused_must_use)] pub fn sample_pool() -> RelayPool { @@ -101,7 +101,7 @@ pub fn test_app() -> Damus { let accounts = get_test_accounts(); for account in accounts { - app.accounts.add_account(account); + app.accounts_mut().add_account(account); } app diff --git a/src/thread.rs b/src/thread.rs @@ -1,14 +1,18 @@ -use crate::note::NoteRef; -use crate::timeline::{TimelineTab, ViewFilter}; -use crate::Error; -use nostrdb::{Filter, FilterBuilder, Ndb, Subscription, Transaction}; +use crate::{ + note::NoteRef, + notecache::NoteCache, + timeline::{TimelineTab, ViewFilter}, + Error, Result, +}; +use enostr::RelayPool; +use nostrdb::{Filter, FilterBuilder, Ndb, Note, Subscription, Transaction}; use std::cmp::Ordering; use std::collections::HashMap; -use tracing::{debug, warn}; +use tracing::{debug, error, info, warn}; #[derive(Default)] pub struct Thread { - pub view: TimelineTab, + view: TimelineTab, sub: Option<Subscription>, remote_sub: Option<String>, pub subscribers: i32, @@ -40,6 +44,48 @@ impl Thread { } } + pub fn view(&self) -> &TimelineTab { + &self.view + } + + pub fn view_mut(&mut self) -> &mut TimelineTab { + &mut self.view + } + + #[must_use = "UnknownIds::update_from_note_refs should be used on this result"] + pub fn poll_notes_into_view<'a>( + &mut self, + txn: &'a Transaction, + ndb: &Ndb, + ) -> Result<Vec<Note<'a>>> { + let sub = self.subscription().expect("thread subscription"); + let new_note_keys = ndb.poll_for_notes(sub, 500); + if new_note_keys.is_empty() { + return Ok(vec![]); + } else { + debug!("{} new notes! {:?}", new_note_keys.len(), new_note_keys); + } + + let mut notes: Vec<Note<'a>> = Vec::with_capacity(new_note_keys.len()); + for key in new_note_keys { + let note = if let Ok(note) = ndb.get_note_by_key(txn, key) { + note + } else { + continue; + }; + + notes.push(note); + } + + { + let reversed = true; + let note_refs: Vec<NoteRef> = notes.iter().map(|n| NoteRef::from_note(n)).collect(); + self.view.insert(&note_refs, reversed); + } + + Ok(notes) + } + /// Look for new thread notes since our last fetch pub fn new_notes( notes: &[NoteRef], @@ -66,7 +112,7 @@ impl Thread { } } - pub fn decrement_sub(&mut self) -> Result<DecrementResult, Error> { + pub fn decrement_sub(&mut self) -> Result<DecrementResult> { self.subscribers -= 1; match self.subscribers.cmp(&0) { @@ -165,7 +211,7 @@ impl Threads { // also use hashbrown? if self.root_id_to_thread.contains_key(root_id) { - return ThreadResult::Stale(self.root_id_to_thread.get_mut(root_id).unwrap()); + return ThreadResult::Stale(self.thread_expected_mut(root_id)); } // we don't have the thread, query for it! @@ -198,3 +244,68 @@ impl Threads { //fn thread_by_id(&self, ndb: &Ndb, id: &[u8; 32]) -> &mut Thread { //} } + +/// Local thread unsubscribe +pub fn thread_unsubscribe( + ndb: &Ndb, + threads: &mut Threads, + pool: &mut RelayPool, + note_cache: &mut NoteCache, + id: &[u8; 32], +) { + let (unsubscribe, remote_subid) = { + let txn = Transaction::new(ndb).expect("txn"); + let root_id = crate::note::root_note_id_from_selected_id(ndb, note_cache, &txn, id); + + let thread = threads.thread_mut(ndb, &txn, root_id).get_ptr(); + let unsub = thread.decrement_sub(); + + let mut remote_subid: Option<String> = None; + if let Ok(DecrementResult::LastSubscriber(_subid)) = unsub { + *thread.subscription_mut() = None; + remote_subid = thread.remote_subscription().to_owned(); + *thread.remote_subscription_mut() = None; + } + + (unsub, remote_subid) + }; + + match unsubscribe { + Ok(DecrementResult::LastSubscriber(sub)) => { + if let Err(e) = ndb.unsubscribe(sub) { + error!( + "failed to unsubscribe from thread: {e}, subid:{}, {} active subscriptions", + sub.id(), + ndb.subscription_count() + ); + } else { + info!( + "Unsubscribed from thread subid:{}. {} active subscriptions", + sub.id(), + ndb.subscription_count() + ); + } + + // unsub from remote + if let Some(subid) = remote_subid { + pool.unsubscribe(subid); + } + } + + Ok(DecrementResult::ActiveSubscribers) => { + info!( + "Keeping thread subscription. {} active subscriptions.", + ndb.subscription_count() + ); + // do nothing + } + + Err(e) => { + // something is wrong! + error!( + "Thread unsubscribe error: {e}. {} active subsciptions.", + ndb.subscription_count() + ); + } + } +} diff --git a/src/timeline/mod.rs b/src/timeline/mod.rs @@ -1,8 +1,6 @@ -use crate::column::Columns; use crate::error::Error; use crate::note::NoteRef; use crate::notecache::{CachedNote, NoteCache}; -use crate::thread::Threads; use crate::unknowns::UnknownIds; use crate::Result; use crate::{filter, filter::FilterState}; @@ -18,9 +16,11 @@ use std::rc::Rc; use tracing::{debug, error}; -mod kind; +pub mod kind; +pub mod route; pub use kind::{PubkeySource, TimelineKind}; +pub use route::TimelineRoute; #[derive(Debug, Hash, Copy, Clone, Eq, PartialEq)] pub struct TimelineId(u32); @@ -37,147 +37,6 @@ impl fmt::Display for TimelineId { } } -#[derive(Debug, Copy, Clone)] -pub enum TimelineSource<'a> { - Column(TimelineId), - Thread(&'a [u8; 32]), -} - -impl<'a> TimelineSource<'a> { - pub fn column(id: TimelineId) -> Self { - TimelineSource::Column(id) - } - - pub fn view<'b>( - self, - ndb: &Ndb, - columns: &'b mut Columns, - threads: &'b mut Threads, - txn: &Transaction, - filter: ViewFilter, - ) -> &'b mut TimelineTab { - match self { - TimelineSource::Column(tid) => columns - .find_timeline_mut(tid) - .expect("timeline") - .view_mut(filter), - - TimelineSource::Thread(root_id) => { - // TODO: replace all this with the raw entry api eventually - - let thread = if threads.root_id_to_thread.contains_key(root_id) { - threads.thread_expected_mut(root_id) - } else { - threads.thread_mut(ndb, txn, root_id).get_ptr() - }; - - &mut thread.view - } - } - } - - fn sub( - self, - ndb: &Ndb, - columns: &Columns, - txn: &Transaction, - threads: &mut Threads, - ) -> Option<Subscription> { - match self { - TimelineSource::Column(tid) => columns.find_timeline(tid).expect("thread").subscription, - TimelineSource::Thread(root_id) => { - // TODO: replace all this with the raw entry api eventually - - let thread = if threads.root_id_to_thread.contains_key(root_id) { - threads.thread_expected_mut(root_id) - } else { - threads.thread_mut(ndb, txn, root_id).get_ptr() - }; - - thread.subscription() - } - } - } - - /// Check local subscriptions for new notes and insert them into - /// timelines (threads, columns) - pub fn poll_notes_into_view( - &self, - txn: &Transaction, - ndb: &Ndb, - columns: &mut Columns, - threads: &mut Threads, - unknown_ids: &mut UnknownIds, - note_cache: &mut NoteCache, - ) -> Result<()> { - let sub = if let Some(sub) = self.sub(ndb, columns, txn, threads) { - sub - } else { - return Err(Error::no_active_sub()); - }; - - let new_note_ids = ndb.poll_for_notes(sub, 100); - if new_note_ids.is_empty() { - return Ok(()); - } else { - debug!("{} new notes! {:?}", new_note_ids.len(), new_note_ids); - } - - let mut new_refs: Vec<(Note, NoteRef)> = Vec::with_capacity(new_note_ids.len()); - - for key in new_note_ids { - let note = if let Ok(note) = ndb.get_note_by_key(txn, key) { - note - } else { - error!("hit race condition in poll_notes_into_view: https://github.com/damus-io/nostrdb/issues/35 note {:?} was not added to timeline", key); - continue; - }; - - UnknownIds::update_from_note(txn, ndb, unknown_ids, note_cache, &note); - - let created_at = note.created_at(); - new_refs.push((note, NoteRef { key, created_at })); - } - - // We're assuming reverse-chronological here (timelines). This - // flag ensures we trigger the items_inserted_at_start - // optimization in VirtualList. We need this flag because we can - // insert notes into chronological order sometimes, and this - // optimization doesn't make sense in those situations. - let reversed = false; - - // ViewFilter::NotesAndReplies - { - let refs: Vec<NoteRef> = new_refs.iter().map(|(_note, nr)| *nr).collect(); - - let reversed = false; - self.view(ndb, columns, threads, txn, ViewFilter::NotesAndReplies) - .insert(&refs, reversed); - } - - // - // handle the filtered case (ViewFilter::Notes, no replies) - // - // TODO(jb55): this is mostly just copied from above, let's just use a loop - // I initially tried this but ran into borrow checker issues - { - let mut filtered_refs = Vec::with_capacity(new_refs.len()); - for (note, nr) in &new_refs { - let cached_note = note_cache.cached_note_or_insert(nr.key, note); - - if ViewFilter::filter_notes(cached_note, note) { - filtered_refs.push(*nr); - } - } - - self.view(ndb, columns, threads, txn, ViewFilter::Notes) - .insert(&filtered_refs, reversed); - } - - Ok(()) - } -} - #[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] pub enum ViewFilter { Notes, @@ -379,6 +238,80 @@ impl Timeline { pub fn view_mut(&mut self, view: ViewFilter) -> &mut TimelineTab { &mut self.views[view.index()] } + + pub fn poll_notes_into_view( + timeline_idx: usize, + timelines: &mut [Timeline], + ndb: &Ndb, + txn: &Transaction, + unknown_ids: &mut UnknownIds, + note_cache: &mut NoteCache, + ) -> Result<()> { + let timeline = &mut timelines[timeline_idx]; + let sub = timeline.subscription.ok_or(Error::no_active_sub())?; + + let new_note_ids = ndb.poll_for_notes(sub, 500); + if new_note_ids.is_empty() { + return Ok(()); + } else { + debug!("{} new notes! {:?}", new_note_ids.len(), new_note_ids); + } + + let mut new_refs: Vec<(Note, NoteRef)> = Vec::with_capacity(new_note_ids.len()); + + for key in new_note_ids { + let note = if let Ok(note) = ndb.get_note_by_key(txn, key) { + note + } else { + error!("hit race condition in poll_notes_into_view: https://github.com/damus-io/nostrdb/issues/35 note {:?} was not added to timeline", key); + continue; + }; + + UnknownIds::update_from_note(txn, ndb, unknown_ids, note_cache, &note); + + let created_at = note.created_at(); + new_refs.push((note, NoteRef { key, created_at })); + } + + // We're assuming reverse-chronological here (timelines). This + // flag ensures we trigger the items_inserted_at_start + // optimization in VirtualList. We need this flag because we can + // insert notes into chronological order sometimes, and this + // optimization doesn't make sense in those situations. + let reversed = false; + + // ViewFilter::NotesAndReplies + { + let refs: Vec<NoteRef> = new_refs.iter().map(|(_note, nr)| *nr).collect(); + + let reversed = false; + timeline + .view_mut(ViewFilter::NotesAndReplies) + .insert(&refs, reversed); + } + + // + // handle the filtered case (ViewFilter::Notes, no replies) + // + // TODO(jb55): this is mostly just copied from above, let's just use a loop + // I initially tried this but ran into borrow checker issues + { + let mut filtered_refs = Vec::with_capacity(new_refs.len()); + for (note, nr) in &new_refs { + let cached_note = note_cache.cached_note_or_insert(nr.key, note); + + if ViewFilter::filter_notes(cached_note, note) { + filtered_refs.push(*nr); + } + } + + timeline + .view_mut(ViewFilter::Notes) + .insert(&filtered_refs, reversed); + } + + Ok(()) + } } pub enum MergeKind { diff --git a/src/timeline/route.rs b/src/timeline/route.rs @@ -0,0 +1,113 @@ +use crate::{ + account_manager::AccountManager, + column::Columns, + draft::Drafts, + imgcache::ImageCache, + notecache::NoteCache, + thread::Threads, + timeline::TimelineId, + ui::{self, note::post::PostResponse}, +}; + +use enostr::{NoteId, RelayPool}; +use nostrdb::{Ndb, Transaction}; + +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +pub enum TimelineRoute { + Timeline(TimelineId), + Thread(NoteId), + Reply(NoteId), +} + +pub enum TimelineRouteResponse { + Post(PostResponse), +} + +impl TimelineRouteResponse { + pub fn post(post: PostResponse) -> Self { + TimelineRouteResponse::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, + note_cache: &mut NoteCache, + threads: &mut Threads, + accounts: &mut AccountManager, + route: TimelineRoute, + col: usize, + show_postbox: bool, + textmode: bool, + ui: &mut egui::Ui, +) -> Option<TimelineRouteResponse> { + 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, ui); + } + } + + if let Some(bar_action) = + ui::TimelineView::new(timeline_id, columns, ndb, note_cache, img_cache, textmode) + .ui(ui) + { + let txn = Transaction::new(ndb).expect("txn"); + let router = columns.columns_mut()[col].router_mut(); + + bar_action.execute_and_process_result(ndb, router, threads, note_cache, pool, &txn); + } + + None + } + + TimelineRoute::Thread(id) => { + if let Some(bar_action) = + ui::ThreadView::new(threads, ndb, note_cache, img_cache, id.bytes(), textmode) + .id_source(egui::Id::new(("threadscroll", col))) + .ui(ui) + { + let txn = Transaction::new(ndb).expect("txn"); + let router = columns.columns_mut()[col].router_mut(); + bar_action.execute_and_process_result(ndb, router, threads, note_cache, pool, &txn); + } + + None + } + + TimelineRoute::Reply(id) => { + let txn = if let Ok(txn) = Transaction::new(ndb) { + txn + } else { + ui.label("Reply to unknown note"); + return None; + }; + + let note = if let Ok(note) = ndb.get_note_by_id(&txn, id.bytes()) { + note + } else { + ui.label("Reply to 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| { + ui::PostReplyView::new(ndb, poster, pool, drafts, note_cache, img_cache, &note) + .id_source(id) + .show(ui) + }); + + Some(TimelineRouteResponse::post(response.inner)) + } else { + None + } + } + } +} diff --git a/src/ui/account_management.rs b/src/ui/account_management.rs @@ -2,6 +2,7 @@ use crate::colors::PINK; use crate::imgcache::ImageCache; use crate::{ account_manager::AccountManager, + route::{Route, Router}, ui::{Preview, PreviewConfig, View}, Damus, }; @@ -12,22 +13,29 @@ use super::profile::preview::SimpleProfilePreview; use super::profile::ProfilePreviewOp; use super::profile_preview_controller::profile_preview_view; -pub struct AccountManagementView {} +pub struct AccountsView<'a> { + ndb: &'a Ndb, + accounts: &'a AccountManager, + img_cache: &'a mut ImageCache, +} #[derive(Clone, Debug)] -pub enum AccountManagementViewResponse { +pub enum AccountsViewResponse { SelectAccount(usize), RemoveAccount(usize), RouteToLogin, } -impl AccountManagementView { - pub fn ui( - ui: &mut Ui, - account_manager: &AccountManager, - ndb: &Ndb, - img_cache: &mut ImageCache, - ) -> InnerResponse<Option<AccountManagementViewResponse>> { +impl<'a> AccountsView<'a> { + pub fn new(ndb: &'a Ndb, accounts: &'a AccountManager, img_cache: &'a mut ImageCache) -> Self { + AccountsView { + ndb, + accounts, + img_cache, + } + } + + pub fn ui(&mut self, ui: &mut Ui) -> InnerResponse<Option<AccountsViewResponse>> { Frame::none().outer_margin(12.0).show(ui, |ui| { if let Some(resp) = Self::top_section_buttons_widget(ui).inner { return Some(resp); @@ -36,7 +44,7 @@ impl AccountManagementView { ui.add_space(8.0); scroll_area() .show(ui, |ui| { - Self::show_accounts(ui, account_manager, ndb, img_cache) + Self::show_accounts(ui, self.accounts, self.ndb, self.img_cache) }) .inner }) @@ -47,8 +55,8 @@ impl AccountManagementView { account_manager: &AccountManager, ndb: &Ndb, img_cache: &mut ImageCache, - ) -> Option<AccountManagementViewResponse> { - let mut return_op: Option<AccountManagementViewResponse> = None; + ) -> Option<AccountsViewResponse> { + let mut return_op: Option<AccountsViewResponse> = None; ui.allocate_ui_with_layout( Vec2::new(ui.available_size_before_wrap().x, 32.0), Layout::top_down(egui::Align::Min), @@ -82,11 +90,9 @@ impl AccountManagementView { profile_preview_view(ui, profile.as_ref(), img_cache, is_selected) { return_op = Some(match op { - ProfilePreviewOp::SwitchTo => { - AccountManagementViewResponse::SelectAccount(i) - } + ProfilePreviewOp::SwitchTo => AccountsViewResponse::SelectAccount(i), ProfilePreviewOp::RemoveAccount => { - AccountManagementViewResponse::RemoveAccount(i) + AccountsViewResponse::RemoveAccount(i) } }); } @@ -98,21 +104,18 @@ impl AccountManagementView { fn top_section_buttons_widget( ui: &mut egui::Ui, - ) -> InnerResponse<Option<AccountManagementViewResponse>> { - ui.horizontal(|ui| { - ui.allocate_ui_with_layout( - Vec2::new(ui.available_size_before_wrap().x, 32.0), - Layout::left_to_right(egui::Align::Center), - |ui| { - if ui.add(add_account_button()).clicked() { - Some(AccountManagementViewResponse::RouteToLogin) - } else { - None - } - }, - ) - .inner - }) + ) -> InnerResponse<Option<AccountsViewResponse>> { + ui.allocate_ui_with_layout( + Vec2::new(ui.available_size_before_wrap().x, 32.0), + Layout::left_to_right(egui::Align::Center), + |ui| { + if ui.add(add_account_button()).clicked() { + Some(AccountsViewResponse::RouteToLogin) + } else { + None + } + }, + ) } } @@ -206,41 +209,41 @@ fn selected_widget() -> impl egui::Widget { mod preview { use super::*; - use crate::{account_manager::process_management_view_response_stateless, test_data}; + use crate::{account_manager::process_accounts_view_response, test_data}; - pub struct AccountManagementPreview { + pub struct AccountsPreview { app: Damus, + router: Router<Route>, } - impl AccountManagementPreview { + impl AccountsPreview { fn new() -> Self { let app = test_data::test_app(); + let router = Router::new(vec![Route::accounts()]); - AccountManagementPreview { app } + AccountsPreview { app, router } } } - impl View for AccountManagementPreview { + impl View for AccountsPreview { fn ui(&mut self, ui: &mut egui::Ui) { ui.add_space(24.0); - if let Some(response) = AccountManagementView::ui( - ui, - &self.app.accounts, - &self.app.ndb, - &mut self.app.img_cache, - ) - .inner + // TODO(jb55): maybe just use render_nav here so we can step through routes + if let Some(response) = + AccountsView::new(&self.app.ndb, &self.app.accounts, &mut self.app.img_cache) + .ui(ui) + .inner { - process_management_view_response_stateless(&mut self.app.accounts, response) + process_accounts_view_response(self.app.accounts_mut(), response, &mut self.router); } } } - impl Preview for AccountManagementView { - type Prev = AccountManagementPreview; + impl<'a> Preview for AccountsView<'a> { + type Prev = AccountsPreview; fn preview(_cfg: PreviewConfig) -> Self::Prev { - AccountManagementPreview::new() + AccountsPreview::new() } } } diff --git a/src/ui/account_switcher.rs b/src/ui/account_switcher.rs @@ -1,6 +1,6 @@ use crate::{ - account_manager::UserAccount, colors::PINK, profile::DisplayName, ui, - ui::profile_preview_controller, Damus, Result, + colors::PINK, profile::DisplayName, ui, ui::profile_preview_controller, + user_account::UserAccount, Damus, Result, }; use nostrdb::Ndb; @@ -48,17 +48,19 @@ impl AccountSelectionWidget { fn perform_action(app: &mut Damus, action: AccountSelectAction) { match action { - AccountSelectAction::RemoveAccount { _index } => app.accounts.remove_account(_index), + AccountSelectAction::RemoveAccount { _index } => { + app.accounts_mut().remove_account(_index) + } AccountSelectAction::SelectAccount { _index } => { app.show_account_switcher = false; - app.accounts.select_account(_index); + app.accounts_mut().select_account(_index); } } } fn show(app: &mut Damus, ui: &mut egui::Ui) -> (AccountSelectResponse, egui::Response) { let mut res = AccountSelectResponse::default(); - let mut selected_index = app.accounts.get_selected_account_index(); + let mut selected_index = app.accounts().get_selected_account_index(); let response = Frame::none() .outer_margin(8.0) @@ -75,9 +77,9 @@ impl AccountSelectionWidget { ui.add(add_account_button()); if let Some(_index) = selected_index { - if let Some(account) = app.accounts.get_account(_index) { + if let Some(account) = app.accounts().get_account(_index) { ui.add_space(8.0); - if Self::handle_sign_out(&app.ndb, ui, account) { + if Self::handle_sign_out(app.ndb(), ui, account) { res.action = Some(AccountSelectAction::RemoveAccount { _index }) } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs @@ -8,12 +8,11 @@ pub mod preview; pub mod profile; pub mod relay; pub mod side_panel; -pub mod stateful_account_management; pub mod thread; pub mod timeline; pub mod username; -pub use account_management::AccountManagementView; +pub use account_management::AccountsView; pub use account_switcher::AccountSelectionWidget; pub use mention::Mention; pub use note::{NoteResponse, NoteView, PostReplyView, PostView}; diff --git a/src/ui/note/mod.rs b/src/ui/note/mod.rs @@ -17,6 +17,7 @@ use crate::{ ui::View, }; use egui::{Label, RichText, Sense}; +use enostr::NoteId; use nostrdb::{Ndb, Note, NoteKey, NoteReply, Transaction}; pub struct NoteView<'a> { @@ -393,7 +394,7 @@ impl<'a> NoteView<'a> { )); if self.options().has_actionbar() { - note_action = render_note_actionbar(ui, note_key).inner; + note_action = render_note_actionbar(ui, self.note.id(), note_key).inner; } resp @@ -430,7 +431,7 @@ impl<'a> NoteView<'a> { )); if self.options().has_actionbar() { - note_action = render_note_actionbar(ui, note_key).inner; + note_action = render_note_actionbar(ui, self.note.id(), note_key).inner; } }); }) @@ -446,6 +447,7 @@ impl<'a> NoteView<'a> { fn render_note_actionbar( ui: &mut egui::Ui, + note_id: &[u8; 32], note_key: NoteKey, ) -> egui::InnerResponse<Option<BarAction>> { ui.horizontal(|ui| { @@ -453,9 +455,9 @@ fn render_note_actionbar( let thread_resp = thread_button(ui, note_key); if reply_resp.clicked() { - Some(BarAction::Reply) + Some(BarAction::Reply(NoteId::new(*note_id))) } else if thread_resp.clicked() { - Some(BarAction::OpenThread) + Some(BarAction::OpenThread(NoteId::new(*note_id))) } else { None } diff --git a/src/ui/profile/profile_preview_controller.rs b/src/ui/profile/profile_preview_controller.rs @@ -41,32 +41,32 @@ pub fn view_profile_previews( ) -> Option<usize> { let width = ui.available_width(); - let txn = if let Ok(txn) = Transaction::new(&app.ndb) { + let txn = if let Ok(txn) = Transaction::new(app.ndb()) { txn } else { return None; }; - for i in 0..app.accounts.num_accounts() { - let account = if let Some(account) = app.accounts.get_account(i) { + for i in 0..app.accounts().num_accounts() { + let account = if let Some(account) = app.accounts().get_account(i) { account } else { continue; }; let profile = app - .ndb + .ndb() .get_profile_by_pubkey(&txn, account.pubkey.bytes()) .ok(); - let preview = SimpleProfilePreview::new(profile.as_ref(), &mut app.img_cache); - - let is_selected = if let Some(selected) = app.accounts.get_selected_account_index() { + let is_selected = if let Some(selected) = app.accounts().get_selected_account_index() { i == selected } else { false }; + let preview = SimpleProfilePreview::new(profile.as_ref(), app.img_cache_mut()); + if add_preview_ui(ui, preview, width, is_selected, i) { return Some(i); } @@ -91,16 +91,16 @@ pub fn show_with_selected_pfp( ui: &mut egui::Ui, ui_element: fn(ui: &mut egui::Ui, pfp: ProfilePic) -> egui::Response, ) -> Option<egui::Response> { - let selected_account = app.accounts.get_selected_account(); + let selected_account = app.accounts().get_selected_account(); if let Some(selected_account) = selected_account { - if let Ok(txn) = Transaction::new(&app.ndb) { + if let Ok(txn) = Transaction::new(app.ndb()) { let profile = app - .ndb + .ndb() .get_profile_by_pubkey(&txn, selected_account.pubkey.bytes()); return Some(ui_element( ui, - ProfilePic::new(&mut app.img_cache, get_profile_url(profile.ok().as_ref())), + ProfilePic::new(app.img_cache_mut(), get_profile_url(profile.ok().as_ref())), )); } } @@ -114,12 +114,12 @@ pub fn show_with_pfp( key: &[u8; 32], ui_element: fn(ui: &mut egui::Ui, pfp: ProfilePic) -> egui::Response, ) -> Option<egui::Response> { - if let Ok(txn) = Transaction::new(&app.ndb) { - let profile = app.ndb.get_profile_by_pubkey(&txn, key); + if let Ok(txn) = Transaction::new(app.ndb()) { + let profile = app.ndb().get_profile_by_pubkey(&txn, key); return Some(ui_element( ui, - ProfilePic::new(&mut app.img_cache, get_profile_url(profile.ok().as_ref())), + ProfilePic::new(app.img_cache_mut(), get_profile_url(profile.ok().as_ref())), )); } None diff --git a/src/ui/stateful_account_management.rs b/src/ui/stateful_account_management.rs @@ -1,131 +0,0 @@ -use egui::Ui; -use egui_nav::{Nav, NavAction}; -use nostrdb::Ndb; - -use crate::{ - account_manager::{process_login_view_response, AccountManager}, - imgcache::ImageCache, - login_manager::LoginState, - routable_widget_state::RoutableWidgetState, - route::{ManageAccountRoute, ManageAcountRouteResponse}, - Damus, -}; - -use super::{ - account_login_view::AccountLoginView, account_management::AccountManagementViewResponse, - AccountManagementView, -}; - -pub struct StatefulAccountManagementView {} - -impl StatefulAccountManagementView { - pub fn show( - ui: &mut Ui, - account_management_state: &mut RoutableWidgetState<ManageAccountRoute>, - account_manager: &mut AccountManager, - img_cache: &mut ImageCache, - login_state: &mut LoginState, - ndb: &Ndb, - ) { - let routes = account_management_state.get_routes(); - - let nav_response = - Nav::new(routes) - .title(false) - .navigating(false) - .show_mut(ui, |ui, nav| match nav.top() { - ManageAccountRoute::AccountManagement => { - AccountManagementView::ui(ui, account_manager, ndb, img_cache) - .inner - .map(ManageAcountRouteResponse::AccountManagement) - } - ManageAccountRoute::AddAccount => AccountLoginView::new(login_state) - .ui(ui) - .inner - .map(ManageAcountRouteResponse::AddAccount), - }); - - if let Some(resp) = nav_response.inner { - match resp { - ManageAcountRouteResponse::AccountManagement(response) => { - process_management_view_response_stateful( - response, - account_manager, - account_management_state, - ); - } - ManageAcountRouteResponse::AddAccount(response) => { - process_login_view_response(account_manager, response); - *login_state = Default::default(); - account_management_state.go_back(); - } - } - } - if let Some(NavAction::Returned) = nav_response.action { - account_management_state.go_back(); - } - } -} - -pub fn process_management_view_response_stateful( - response: AccountManagementViewResponse, - manager: &mut AccountManager, - state: &mut RoutableWidgetState<ManageAccountRoute>, -) { - match response { - AccountManagementViewResponse::RemoveAccount(index) => { - manager.remove_account(index); - } - AccountManagementViewResponse::SelectAccount(index) => { - manager.select_account(index); - } - AccountManagementViewResponse::RouteToLogin => { - state.route_to(ManageAccountRoute::AddAccount); - } - } -} - -mod preview { - use crate::{ - test_data, - ui::{Preview, PreviewConfig, View}, - }; - - use super::*; - - pub struct StatefulAccountManagementPreview { - app: Damus, - } - - impl StatefulAccountManagementPreview { - fn new() -> Self { - let mut app = test_data::test_app(); - app.account_management_view_state - .route_to(ManageAccountRoute::AccountManagement); - - StatefulAccountManagementPreview { app } - } - } - - impl View for StatefulAccountManagementPreview { - fn ui(&mut self, ui: &mut egui::Ui) { - StatefulAccountManagementView::show( - ui, - &mut self.app.account_management_view_state, - &mut self.app.accounts, - &mut self.app.img_cache, - &mut self.app.login_state, - &self.app.ndb, - ); - } - } - - impl Preview for StatefulAccountManagementView { - type Prev = StatefulAccountManagementPreview; - - fn preview(cfg: PreviewConfig) -> Self::Prev { - let _ = cfg; - StatefulAccountManagementPreview::new() - } - } -} diff --git a/src/ui/thread.rs b/src/ui/thread.rs @@ -1,55 +1,49 @@ use crate::{ - actionbar::BarResult, column::Columns, imgcache::ImageCache, notecache::NoteCache, - thread::Threads, timeline::TimelineSource, ui, unknowns::UnknownIds, + actionbar::BarAction, imgcache::ImageCache, notecache::NoteCache, thread::Threads, ui, }; -use enostr::RelayPool; use nostrdb::{Ndb, NoteKey, Transaction}; use tracing::{error, warn}; pub struct ThreadView<'a> { - column: usize, - columns: &'a mut Columns, threads: &'a mut Threads, ndb: &'a Ndb, - pool: &'a mut RelayPool, note_cache: &'a mut NoteCache, img_cache: &'a mut ImageCache, - unknown_ids: &'a mut UnknownIds, selected_note_id: &'a [u8; 32], textmode: bool, + id_source: egui::Id, } impl<'a> ThreadView<'a> { #[allow(clippy::too_many_arguments)] pub fn new( - column: usize, - columns: &'a mut Columns, threads: &'a mut Threads, ndb: &'a Ndb, note_cache: &'a mut NoteCache, img_cache: &'a mut ImageCache, - unknown_ids: &'a mut UnknownIds, - pool: &'a mut RelayPool, - textmode: bool, selected_note_id: &'a [u8; 32], + textmode: bool, ) -> Self { + let id_source = egui::Id::new("threadscroll_threadview"); ThreadView { - column, - columns, threads, ndb, note_cache, img_cache, - textmode, selected_note_id, - unknown_ids, - pool, + textmode, + id_source, } } - pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<BarResult> { + pub fn id_source(mut self, id: egui::Id) -> Self { + self.id_source = id; + self + } + + pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<BarAction> { let txn = Transaction::new(self.ndb).expect("txn"); - let mut result: Option<BarResult> = None; + let mut action: Option<BarAction> = None; let selected_note_key = if let Ok(key) = self .ndb @@ -62,21 +56,13 @@ impl<'a> ThreadView<'a> { return None; }; - let scroll_id = { - egui::Id::new(( - "threadscroll", - self.columns.column(self.column).view_id(), - selected_note_key, - )) - }; - ui.label( egui::RichText::new("Threads ALPHA! It's not done. Things will be broken.") .color(egui::Color32::RED), ); egui::ScrollArea::vertical() - .id_source(scroll_id) + .id_source(self.id_source) .animated(false) .auto_shrink([false, false]) .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible) @@ -99,36 +85,27 @@ impl<'a> ThreadView<'a> { .map_or_else(|| self.selected_note_id, |nr| nr.id) }; + let thread = self.threads.thread_mut(self.ndb, &txn, root_id).get_ptr(); + + // TODO(jb55): skip poll if ThreadResult is fresh? + // poll for new notes and insert them into our existing notes - if let Err(e) = TimelineSource::Thread(root_id).poll_notes_into_view( - &txn, - self.ndb, - self.columns, - self.threads, - self.unknown_ids, - self.note_cache, - ) { + if let Err(e) = thread.poll_notes_into_view(&txn, self.ndb) { error!("Thread::poll_notes_into_view: {e}"); } - let (len, list) = { - let thread = self.threads.thread_mut(self.ndb, &txn, root_id).get_ptr(); + let len = thread.view().notes.len(); - let len = thread.view.notes.len(); - (len, &mut thread.view.list) - }; - - list.clone() - .borrow_mut() - .ui_custom_layout(ui, len, |ui, start_index| { + thread.view().list.clone().borrow_mut().ui_custom_layout( + ui, + len, + |ui, start_index| { ui.spacing_mut().item_spacing.y = 0.0; ui.spacing_mut().item_spacing.x = 4.0; let ind = len - 1 - start_index; - let note_key = { - let thread = self.threads.thread_mut(self.ndb, &txn, root_id).get_ptr(); - thread.view.notes[ind].key - }; + + let note_key = thread.view().notes[ind].key; let note = if let Ok(note) = self.ndb.get_note_by_key(&txn, note_key) { note @@ -138,25 +115,14 @@ impl<'a> ThreadView<'a> { }; ui::padding(8.0, ui, |ui| { - let resp = + if let Some(bar_action) = ui::NoteView::new(self.ndb, self.note_cache, self.img_cache, &note) .note_previews(!self.textmode) .textmode(self.textmode) - .show(ui); - - if let Some(action) = resp.action { - let br = action.execute( - self.ndb, - self.columns.column_mut(self.column), - self.threads, - self.note_cache, - self.pool, - note.id(), - &txn, - ); - if br.is_some() { - result = br; - } + .show(ui) + .action + { + action = Some(bar_action); } }); @@ -164,9 +130,10 @@ impl<'a> ThreadView<'a> { //ui.add(egui::Separator::default().spacing(0.0)); 1 - }); + }, + ); }); - result + action } } diff --git a/src/ui/timeline.rs b/src/ui/timeline.rs @@ -1,67 +1,56 @@ use crate::{ - actionbar::BarAction, - actionbar::BarResult, - column::{Column, ColumnKind}, - draft::Drafts, - imgcache::ImageCache, - notecache::NoteCache, - thread::Threads, - ui, - ui::note::PostAction, + actionbar::BarAction, column::Columns, draft::Drafts, imgcache::ImageCache, + notecache::NoteCache, timeline::TimelineId, ui, ui::note::PostAction, }; use egui::containers::scroll_area::ScrollBarVisibility; use egui::{Direction, Layout}; use egui_tabs::TabColor; use enostr::{FilledKeypair, RelayPool}; -use nostrdb::{Ndb, Note, Transaction}; -use tracing::{debug, info, warn}; +use nostrdb::{Ndb, Transaction}; +use tracing::{debug, error, info, warn}; pub struct TimelineView<'a> { + timeline_id: TimelineId, + columns: &'a mut Columns, ndb: &'a Ndb, - column: &'a mut Column, note_cache: &'a mut NoteCache, img_cache: &'a mut ImageCache, - threads: &'a mut Threads, - pool: &'a mut RelayPool, textmode: bool, reverse: bool, } impl<'a> TimelineView<'a> { pub fn new( + timeline_id: TimelineId, + columns: &'a mut Columns, ndb: &'a Ndb, - column: &'a mut Column, note_cache: &'a mut NoteCache, img_cache: &'a mut ImageCache, - threads: &'a mut Threads, - pool: &'a mut RelayPool, textmode: bool, ) -> TimelineView<'a> { let reverse = false; TimelineView { ndb, - column, + timeline_id, + columns, note_cache, img_cache, - threads, - pool, reverse, textmode, } } - pub fn ui(&mut self, ui: &mut egui::Ui) { + pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<BarAction> { timeline_ui( ui, self.ndb, - self.column, + self.timeline_id, + self.columns, self.note_cache, self.img_cache, - self.threads, - self.pool, self.reverse, self.textmode, - ); + ) } pub fn reversed(mut self) -> Self { @@ -74,14 +63,13 @@ impl<'a> TimelineView<'a> { fn timeline_ui( ui: &mut egui::Ui, ndb: &Ndb, - column: &mut Column, + timeline_id: TimelineId, + columns: &mut Columns, note_cache: &mut NoteCache, img_cache: &mut ImageCache, - threads: &mut Threads, - pool: &mut RelayPool, reversed: bool, textmode: bool, -) { +) -> Option<BarAction> { //padding(4.0, ui, |ui| ui.heading("Notifications")); /* let font_id = egui::TextStyle::Body.resolve(ui.style()); @@ -89,29 +77,37 @@ fn timeline_ui( */ - { - let timeline = if let ColumnKind::Timeline(timeline) = column.kind_mut() { + let scroll_id = { + let timeline = if let Some(timeline) = columns.find_timeline_mut(timeline_id) { timeline } else { - return; + error!("tried to render timeline in column, but timeline was missing"); + // TODO (jb55): render error when timeline is missing? + // this shouldn't happen... + return None; }; timeline.selected_view = tabs_ui(ui); // need this for some reason?? ui.add_space(3.0); - } - let scroll_id = egui::Id::new(("tlscroll", column.view_id())); + egui::Id::new(("tlscroll", timeline.view_id())) + }; + + let mut bar_action: Option<BarAction> = None; egui::ScrollArea::vertical() .id_source(scroll_id) .animated(false) .auto_shrink([false, false]) .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible) .show(ui, |ui| { - let timeline = if let ColumnKind::Timeline(timeline) = column.kind_mut() { + let timeline = if let Some(timeline) = columns.find_timeline_mut(timeline_id) { timeline } else { + error!("tried to render timeline in column, but timeline was missing"); + // TODO (jb55): render error when timeline is missing? + // this shouldn't happen... return 0; }; @@ -124,7 +120,6 @@ fn timeline_ui( return 0; }; - let mut bar_action: Option<(BarAction, Note)> = None; view.list .clone() .borrow_mut() @@ -154,7 +149,7 @@ fn timeline_ui( .show(ui); if let Some(ba) = resp.action { - bar_action = Some((ba, note)); + bar_action = Some(ba); } else if resp.response.clicked() { debug!("clicked note"); } @@ -166,25 +161,10 @@ fn timeline_ui( 1 }); - // handle any actions from the virtual list - if let Some((action, note)) = bar_action { - if let Some(br) = - action.execute(ndb, column, threads, note_cache, pool, note.id(), &txn) - { - match br { - // update the thread for next render if we have new notes - BarResult::NewThreadNotes(new_notes) => { - let thread = threads - .thread_mut(ndb, &txn, new_notes.root_id.bytes()) - .get_ptr(); - new_notes.process(thread); - } - } - } - } - 1 }); + + bar_action } pub fn postbox_view<'a>( diff --git a/src/ui_preview/main.rs b/src/ui_preview/main.rs @@ -1,11 +1,10 @@ use notedeck::app_creation::{ generate_mobile_emulator_native_options, generate_native_options, setup_cc, }; -use notedeck::ui::account_login_view::AccountLoginView; -use notedeck::ui::stateful_account_management::StatefulAccountManagementView; use notedeck::ui::{ - AccountManagementView, AccountSelectionWidget, DesktopSidePanel, PostView, Preview, PreviewApp, - PreviewConfig, ProfilePic, ProfilePreview, RelayView, + account_login_view::AccountLoginView, account_management::AccountsView, AccountSelectionWidget, + DesktopSidePanel, PostView, Preview, PreviewApp, PreviewConfig, ProfilePic, ProfilePreview, + RelayView, }; use std::env; @@ -101,10 +100,9 @@ async fn main() { AccountLoginView, ProfilePreview, ProfilePic, - AccountManagementView, + AccountsView, AccountSelectionWidget, DesktopSidePanel, PostView, - StatefulAccountManagementView, ); } diff --git a/src/unknowns.rs b/src/unknowns.rs @@ -1,7 +1,11 @@ -use crate::column::Columns; -use crate::notecache::{CachedNote, NoteCache}; -use crate::timeline::ViewFilter; -use crate::Result; +use crate::{ + column::Columns, + note::NoteRef, + notecache::{CachedNote, NoteCache}, + timeline::ViewFilter, + Result, +}; + use enostr::{Filter, NoteId, Pubkey}; use nostrdb::{BlockType, Mention, Ndb, Note, NoteKey, Transaction}; use std::collections::HashSet; @@ -64,6 +68,35 @@ impl UnknownIds { self.last_updated = Some(now); } + pub fn update_from_note_key( + txn: &Transaction, + ndb: &Ndb, + unknown_ids: &mut UnknownIds, + note_cache: &mut NoteCache, + key: NoteKey, + ) -> bool { + let note = if let Ok(note) = ndb.get_note_by_key(txn, key) { + note + } else { + return false; + }; + + UnknownIds::update_from_note(txn, ndb, unknown_ids, note_cache, &note) + } + + /// Should be called on freshly polled notes from subscriptions + pub fn update_from_note_refs( + txn: &Transaction, + ndb: &Ndb, + unknown_ids: &mut UnknownIds, + note_cache: &mut NoteCache, + note_refs: &[NoteRef], + ) { + for note_ref in note_refs { + Self::update_from_note_key(txn, ndb, unknown_ids, note_cache, note_ref.key); + } + } + pub fn update_from_note( txn: &Transaction, ndb: &Ndb, diff --git a/src/view_state.rs b/src/view_state.rs @@ -0,0 +1,13 @@ +use crate::login_manager::LoginState; + +/// Various state for views +#[derive(Default)] +pub struct ViewState { + pub login: LoginState, +} + +impl ViewState { + pub fn login_mut(&mut self) -> &mut LoginState { + &mut self.login + } +}