diff --git a/.envrc b/.envrc index ec7378d7..df6cf659 100644 --- 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 index 9c7db5b0..17d87064 100644 --- 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, +) { + 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, 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 index f46d747f..128909bc 100644 --- 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, note_cache: &mut NoteCache, pool: &mut RelayPool, threads: &mut Threads, selected_note: &[u8; 32], ) -> Option { { - 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, threads: &mut Threads, note_cache: &mut NoteCache, pool: &mut RelayPool, - replying_to: &[u8; 32], txn: &Transaction, ) -> Option { 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, + 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, 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 index ab0c73de..9fe6a2ea 100644 --- 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, 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 = 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>(data_path: P) -> Self { - let mut columns: Vec = 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 = 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, - ¬e, - ) - .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 index 496a2b53..16e4d5b0 100644 --- 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, - - pub navigating: bool, - pub returning: bool, + router: Router, } 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 new(routes: Vec) -> Self { + let router = Router::new(routes); + Column { router } } - pub fn kind(&self) -> &ColumnKind { - &self.kind + pub fn router(&self) -> &Router { + &self.router } - pub fn kind_mut(&mut self) -> &mut ColumnKind { - &mut self.kind - } - - pub fn view_id(&self) -> egui::Id { - self.kind.view_id() - } - - pub fn routes(&self) -> &[Route] { - &self.routes - } - - pub fn routes_mut(&mut self) -> &mut Vec { - &mut self.routes - } - - pub fn new(kind: ColumnKind, routes: Vec) -> Self { - let navigating = false; - let returning = false; - Column { - kind, - routes, - navigating, - returning, - } + pub fn router_mut(&mut self) -> &mut Router { + &mut self.router } } +#[derive(Default)] pub struct Columns { + /// Columns are simply routers into settings, timelines, etc columns: Vec, + /// 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, + /// 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 { &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 { &self.columns } - pub fn new(columns: Vec) -> 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 { - self.columns - .iter_mut() - .filter_map(|c| c.kind_mut().timeline_mut()) + pub fn timelines_mut(&mut self) -> &mut Vec { + &mut self.timelines } - pub fn timelines(&self) -> impl Iterator { - self.columns.iter().filter_map(|c| c.kind().timeline()) + pub fn timelines(&self) -> &Vec { + &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 index 5d6d0d6e..371aa049 100644 --- 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 index 36897325..33376767 100644 --- 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 index f2393fcd..2971286c 100644 --- 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 new file mode 100644 index 00000000..11536f0d --- /dev/null +++ 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 index 20e32fba..a462a7b4 100644 --- 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 deleted file mode 100644 index 4173367c..00000000 --- a/src/routable_widget_state.rs +++ /dev/null @@ -1,26 +0,0 @@ -#[derive(Default)] -pub struct RoutableWidgetState { - routes: Vec, -} - -impl RoutableWidgetState { - 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 { - self.routes.last().cloned() - } - - pub fn get_routes(&self) -> Vec { - self.routes.clone() - } -} diff --git a/src/route.rs b/src/route.rs index fe02e72e..259132c2 100644 --- 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)) + } + + 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) + } +} + +// TODO: add this to egui-nav so we don't have to deal with returning +// and navigating headaches +#[derive(Clone)] +pub struct Router { + routes: Vec, + pub returning: bool, + pub navigating: bool, +} + +impl Router { + pub fn new(routes: Vec) -> 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 { + 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 { + &self.routes + } } 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::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"), + }, } } } - -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"), - } - } -} - -pub enum ManageAcountRouteResponse { - AccountManagement(AccountManagementViewResponse), - AddAccount(AccountLoginResponse), -} diff --git a/src/test_data.rs b/src/test_data.rs index 77724fba..b271dad3 100644 --- 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 index 0dcdf717..baa747a4 100644 --- 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, remote_sub: Option, 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>> { + 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> = 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 = notes.iter().map(|n| NoteRef::from_note(n)).collect(); + self.view.insert(¬e_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 { + pub fn decrement_sub(&mut self) -> Result { 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 = 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 index 49b60f4b..f5365c8f 100644 --- 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 { - 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, ¬e); - - 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 = 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, ¬e); + + 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 = 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 new file mode 100644 index 00000000..d506004f --- /dev/null +++ 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 { + 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, ¬e) + .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 index 629141da..7814c177 100644 --- 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> { +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> { 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 { - let mut return_op: Option = None; + ) -> Option { + let mut return_op: Option = 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> { - 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> { + 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, } - 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 index ff1fe36a..64548948 100644 --- 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 index ead5f48a..7da506aa 100644 --- 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 index 187d0397..be3b00f7 100644 --- 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> { 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 index 1a940e60..f8bbd7da 100644 --- 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 { 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 { - 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 { - 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 deleted file mode 100644 index 6da1111b..00000000 --- a/src/ui/stateful_account_management.rs +++ /dev/null @@ -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, - 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, -) { - 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 index b2a5d6b0..0abf67f4 100644 --- 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 { + 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 { let txn = Transaction::new(self.ndb).expect("txn"); - let mut result: Option = None; + let mut action: Option = 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, ¬e) .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 index cb113c7d..a9e6854c 100644 --- 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 { 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 { //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 = 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 index eecb5d89..577338bf 100644 --- 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 index 3b2d37a3..a1eabf7c 100644 --- 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, ¬e) + } + + /// 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 new file mode 100644 index 00000000..33b94b30 --- /dev/null +++ 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 + } +}