notedeck

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

nav.rs (37772B)


      1 use crate::{
      2     accounts::{render_accounts_route, AccountsAction},
      3     app::{get_active_columns_mut, get_decks_mut},
      4     column::ColumnsAction,
      5     deck_state::DeckState,
      6     decks::{Deck, DecksAction, DecksCache},
      7     drag::{get_drag_id, get_drag_id_through_frame},
      8     options::AppOptions,
      9     profile::{ProfileAction, SaveProfileChanges},
     10     route::{Route, Router, SingletonRouter},
     11     timeline::{
     12         route::{render_thread_route, render_timeline_route},
     13         TimelineCache, TimelineKind,
     14     },
     15     ui::{
     16         self,
     17         add_column::{render_add_column_routes, AddColumnView},
     18         column::NavTitle,
     19         configure_deck::ConfigureDeckView,
     20         edit_deck::{EditDeckResponse, EditDeckView},
     21         note::{custom_zap::CustomZapView, NewPostAction, PostAction, PostType, QuoteRepostView},
     22         profile::EditProfileView,
     23         search::{FocusState, SearchView},
     24         settings::SettingsAction,
     25         support::SupportView,
     26         wallet::{get_default_zap_state, WalletAction, WalletState, WalletView},
     27         AccountsView, PostReplyView, PostView, ProfileView, RelayView, SettingsView, ThreadView,
     28         TimelineView,
     29     },
     30     Damus,
     31 };
     32 
     33 use egui_nav::{Nav, NavAction, NavResponse, NavUiType, Percent, PopupResponse, PopupSheet};
     34 use enostr::ProfileState;
     35 use nostrdb::{Filter, Ndb, Transaction};
     36 use notedeck::{
     37     get_current_default_msats, tr, ui::is_narrow, Accounts, AppContext, NoteAction, NoteContext,
     38     RelayAction,
     39 };
     40 use tracing::error;
     41 
     42 /// The result of processing a nav response
     43 pub enum ProcessNavResult {
     44     SwitchOccurred,
     45     PfpClicked,
     46 }
     47 
     48 impl ProcessNavResult {
     49     pub fn switch_occurred(&self) -> bool {
     50         matches!(self, Self::SwitchOccurred)
     51     }
     52 }
     53 
     54 #[allow(clippy::enum_variant_names)]
     55 pub enum RenderNavAction {
     56     Back,
     57     RemoveColumn,
     58     /// The response when the user interacts with a pfp in the nav header
     59     PfpClicked,
     60     PostAction(NewPostAction),
     61     NoteAction(NoteAction),
     62     ProfileAction(ProfileAction),
     63     SwitchingAction(SwitchingAction),
     64     WalletAction(WalletAction),
     65     RelayAction(RelayAction),
     66     SettingsAction(SettingsAction),
     67 }
     68 
     69 pub enum SwitchingAction {
     70     Accounts(AccountsAction),
     71     Columns(ColumnsAction),
     72     Decks(crate::decks::DecksAction),
     73 }
     74 
     75 impl SwitchingAction {
     76     /// process the action, and return whether switching occured
     77     pub fn process(
     78         &self,
     79         timeline_cache: &mut TimelineCache,
     80         decks_cache: &mut DecksCache,
     81         ctx: &mut AppContext<'_>,
     82         ui_ctx: &egui::Context,
     83     ) -> bool {
     84         match &self {
     85             SwitchingAction::Accounts(account_action) => match account_action {
     86                 AccountsAction::Switch(switch_action) => {
     87                     let txn = Transaction::new(ctx.ndb).expect("txn");
     88                     ctx.accounts.select_account(
     89                         &switch_action.switch_to,
     90                         ctx.ndb,
     91                         &txn,
     92                         ctx.pool,
     93                         ui_ctx,
     94                     );
     95                     // pop nav after switch
     96                     get_active_columns_mut(ctx.i18n, ctx.accounts, decks_cache)
     97                         .column_mut(switch_action.source_column)
     98                         .router_mut()
     99                         .go_back();
    100                 }
    101                 AccountsAction::Remove(to_remove) => 's: {
    102                     if !ctx
    103                         .accounts
    104                         .remove_account(to_remove, ctx.ndb, ctx.pool, ui_ctx)
    105                     {
    106                         break 's;
    107                     }
    108 
    109                     decks_cache.remove(ctx.i18n, to_remove, timeline_cache, ctx.ndb, ctx.pool);
    110                 }
    111             },
    112             SwitchingAction::Columns(columns_action) => match *columns_action {
    113                 ColumnsAction::Remove(index) => {
    114                     let kinds_to_pop = get_active_columns_mut(ctx.i18n, ctx.accounts, decks_cache)
    115                         .delete_column(index);
    116                     for kind in &kinds_to_pop {
    117                         if let Err(err) = timeline_cache.pop(kind, ctx.ndb, ctx.pool) {
    118                             error!("error popping timeline: {err}");
    119                         }
    120                     }
    121                 }
    122 
    123                 ColumnsAction::Switch(from, to) => {
    124                     get_active_columns_mut(ctx.i18n, ctx.accounts, decks_cache).move_col(from, to);
    125                 }
    126             },
    127             SwitchingAction::Decks(decks_action) => match *decks_action {
    128                 DecksAction::Switch(index) => {
    129                     get_decks_mut(ctx.i18n, ctx.accounts, decks_cache).set_active(index)
    130                 }
    131                 DecksAction::Removing(index) => {
    132                     get_decks_mut(ctx.i18n, ctx.accounts, decks_cache).remove_deck(
    133                         index,
    134                         timeline_cache,
    135                         ctx.ndb,
    136                         ctx.pool,
    137                     );
    138                 }
    139             },
    140         }
    141         true
    142     }
    143 }
    144 
    145 impl From<PostAction> for RenderNavAction {
    146     fn from(post_action: PostAction) -> Self {
    147         match post_action {
    148             PostAction::QuotedNoteAction(note_action) => Self::NoteAction(note_action),
    149             PostAction::NewPostAction(new_post) => Self::PostAction(new_post),
    150         }
    151     }
    152 }
    153 
    154 impl From<NewPostAction> for RenderNavAction {
    155     fn from(post_action: NewPostAction) -> Self {
    156         Self::PostAction(post_action)
    157     }
    158 }
    159 
    160 impl From<NoteAction> for RenderNavAction {
    161     fn from(note_action: NoteAction) -> RenderNavAction {
    162         Self::NoteAction(note_action)
    163     }
    164 }
    165 
    166 enum NotedeckNavResponse {
    167     Popup(Box<PopupResponse<Option<RenderNavAction>>>),
    168     Nav(Box<NavResponse<Option<RenderNavAction>>>),
    169 }
    170 
    171 pub struct RenderNavResponse {
    172     column: usize,
    173     response: NotedeckNavResponse,
    174 }
    175 
    176 impl RenderNavResponse {
    177     #[allow(private_interfaces)]
    178     pub fn new(column: usize, response: NotedeckNavResponse) -> Self {
    179         RenderNavResponse { column, response }
    180     }
    181 
    182     #[must_use = "Make sure to save columns if result is true"]
    183     pub fn process_render_nav_response(
    184         self,
    185         app: &mut Damus,
    186         ctx: &mut AppContext<'_>,
    187         ui: &mut egui::Ui,
    188     ) -> Option<ProcessNavResult> {
    189         match self.response {
    190             NotedeckNavResponse::Popup(nav_action) => {
    191                 process_popup_resp(*nav_action, app, ctx, ui, self.column)
    192             }
    193             NotedeckNavResponse::Nav(nav_response) => {
    194                 process_nav_resp(app, ctx, ui, *nav_response, self.column)
    195             }
    196         }
    197     }
    198 }
    199 
    200 fn process_popup_resp(
    201     action: PopupResponse<Option<RenderNavAction>>,
    202     app: &mut Damus,
    203     ctx: &mut AppContext<'_>,
    204     ui: &mut egui::Ui,
    205     col: usize,
    206 ) -> Option<ProcessNavResult> {
    207     let mut process_result: Option<ProcessNavResult> = None;
    208     if let Some(nav_action) = action.response {
    209         process_result = process_render_nav_action(app, ctx, ui, col, nav_action);
    210     }
    211 
    212     if let Some(NavAction::Returned(_)) = action.action {
    213         let column = app.columns_mut(ctx.i18n, ctx.accounts).column_mut(col);
    214         column.sheet_router.clear();
    215     } else if let Some(NavAction::Navigating) = action.action {
    216         let column = app.columns_mut(ctx.i18n, ctx.accounts).column_mut(col);
    217         column.sheet_router.navigating = false;
    218     }
    219 
    220     process_result
    221 }
    222 
    223 fn process_nav_resp(
    224     app: &mut Damus,
    225     ctx: &mut AppContext<'_>,
    226     ui: &mut egui::Ui,
    227     response: NavResponse<Option<RenderNavAction>>,
    228     col: usize,
    229 ) -> Option<ProcessNavResult> {
    230     let mut process_result: Option<ProcessNavResult> = None;
    231 
    232     if let Some(action) = response.response.or(response.title_response) {
    233         // start returning when we're finished posting
    234 
    235         process_result = process_render_nav_action(app, ctx, ui, col, action);
    236     }
    237 
    238     if let Some(action) = response.action {
    239         match action {
    240             NavAction::Returned(return_type) => {
    241                 let r = app
    242                     .columns_mut(ctx.i18n, ctx.accounts)
    243                     .column_mut(col)
    244                     .router_mut()
    245                     .pop();
    246 
    247                 if let Some(Route::Timeline(kind)) = &r {
    248                     if let Err(err) = app.timeline_cache.pop(kind, ctx.ndb, ctx.pool) {
    249                         error!("popping timeline had an error: {err} for {:?}", kind);
    250                     }
    251                 };
    252 
    253                 if let Some(Route::Thread(selection)) = &r {
    254                     app.threads
    255                         .close(ctx.ndb, ctx.pool, selection, return_type, col);
    256                 }
    257 
    258                 // we should remove profile state once we've returned
    259                 if let Some(Route::EditProfile(pk)) = &r {
    260                     app.view_state.pubkey_to_profile_state.remove(pk);
    261                 }
    262 
    263                 process_result = Some(ProcessNavResult::SwitchOccurred);
    264             }
    265 
    266             NavAction::Navigated => {
    267                 let cur_router = app
    268                     .columns_mut(ctx.i18n, ctx.accounts)
    269                     .column_mut(col)
    270                     .router_mut();
    271                 cur_router.navigating = false;
    272                 if cur_router.is_replacing() {
    273                     cur_router.remove_previous_routes();
    274                 }
    275 
    276                 process_result = Some(ProcessNavResult::SwitchOccurred);
    277             }
    278 
    279             NavAction::Dragging => {}
    280             NavAction::Returning(_) => {}
    281             NavAction::Resetting => {}
    282             NavAction::Navigating => {
    283                 // explicitly update the edit profile state when navigating
    284                 handle_navigating_edit_profile(ctx.ndb, ctx.accounts, app, col);
    285             }
    286         }
    287     }
    288 
    289     process_result
    290 }
    291 
    292 /// We are navigating to edit profile, prepare the profile state
    293 /// if we don't have it
    294 fn handle_navigating_edit_profile(ndb: &Ndb, accounts: &Accounts, app: &mut Damus, col: usize) {
    295     let pk = {
    296         let Route::EditProfile(pk) = app.columns(accounts).column(col).router().top() else {
    297             return;
    298         };
    299 
    300         if app.view_state.pubkey_to_profile_state.contains_key(pk) {
    301             return;
    302         }
    303 
    304         pk.to_owned()
    305     };
    306 
    307     let txn = Transaction::new(ndb).expect("txn");
    308     app.view_state.pubkey_to_profile_state.insert(pk, {
    309         let filter = Filter::new_with_capacity(1)
    310             .kinds([0])
    311             .authors([pk.bytes()])
    312             .build();
    313 
    314         if let Ok(results) = ndb.query(&txn, &[filter], 1) {
    315             if let Some(result) = results.first() {
    316                 tracing::debug!(
    317                     "refreshing profile state for edit view: {}",
    318                     result.note.content()
    319                 );
    320                 ProfileState::from_note_contents(result.note.content())
    321             } else {
    322                 ProfileState::default()
    323             }
    324         } else {
    325             ProfileState::default()
    326         }
    327     });
    328 }
    329 
    330 pub enum RouterAction {
    331     GoBack,
    332     /// We clicked on a pfp in a route. We currently don't carry any
    333     /// information about the pfp since we only use it for toggling the
    334     /// chrome atm
    335     PfpClicked,
    336     RouteTo(Route, RouterType),
    337     Overlay {
    338         route: Route,
    339         make_new: bool,
    340     },
    341 }
    342 
    343 pub enum RouterType {
    344     Sheet,
    345     Stack,
    346 }
    347 
    348 fn go_back(stack: &mut Router<Route>, sheet: &mut SingletonRouter<Route>) {
    349     if sheet.route().is_some() {
    350         sheet.go_back();
    351     } else {
    352         stack.go_back();
    353     }
    354 }
    355 
    356 impl RouterAction {
    357     pub fn process(
    358         self,
    359         stack_router: &mut Router<Route>,
    360         sheet_router: &mut SingletonRouter<Route>,
    361     ) -> Option<ProcessNavResult> {
    362         match self {
    363             RouterAction::GoBack => {
    364                 go_back(stack_router, sheet_router);
    365 
    366                 None
    367             }
    368 
    369             RouterAction::PfpClicked => {
    370                 if stack_router.routes().len() == 1 {
    371                     // if we're at the top level and we click a profile pic,
    372                     // bubble it up so that it can be handled by the chrome
    373                     // to open the sidebar
    374                     Some(ProcessNavResult::PfpClicked)
    375                 } else {
    376                     // Otherwise just execute a back action
    377                     go_back(stack_router, sheet_router);
    378 
    379                     None
    380                 }
    381             }
    382 
    383             RouterAction::RouteTo(route, router_type) => match router_type {
    384                 RouterType::Sheet => {
    385                     sheet_router.route_to(route);
    386                     None
    387                 }
    388                 RouterType::Stack => {
    389                     stack_router.route_to(route);
    390                     None
    391                 }
    392             },
    393             RouterAction::Overlay { route, make_new } => {
    394                 if make_new {
    395                     stack_router.route_to_overlaid_new(route);
    396                 } else {
    397                     stack_router.route_to_overlaid(route);
    398                 }
    399                 None
    400             }
    401         }
    402     }
    403 
    404     pub fn route_to(route: Route) -> Self {
    405         RouterAction::RouteTo(route, RouterType::Stack)
    406     }
    407 
    408     pub fn route_to_sheet(route: Route) -> Self {
    409         RouterAction::RouteTo(route, RouterType::Sheet)
    410     }
    411 }
    412 
    413 fn process_render_nav_action(
    414     app: &mut Damus,
    415     ctx: &mut AppContext<'_>,
    416     ui: &mut egui::Ui,
    417     col: usize,
    418     action: RenderNavAction,
    419 ) -> Option<ProcessNavResult> {
    420     let router_action = match action {
    421         RenderNavAction::Back => Some(RouterAction::GoBack),
    422         RenderNavAction::PfpClicked => Some(RouterAction::PfpClicked),
    423         RenderNavAction::RemoveColumn => {
    424             let kinds_to_pop = app.columns_mut(ctx.i18n, ctx.accounts).delete_column(col);
    425 
    426             for kind in &kinds_to_pop {
    427                 if let Err(err) = app.timeline_cache.pop(kind, ctx.ndb, ctx.pool) {
    428                     error!("error popping timeline: {err}");
    429                 }
    430             }
    431 
    432             return Some(ProcessNavResult::SwitchOccurred);
    433         }
    434         RenderNavAction::PostAction(new_post_action) => {
    435             let txn = Transaction::new(ctx.ndb).expect("txn");
    436             match new_post_action.execute(ctx.ndb, &txn, ctx.pool, &mut app.drafts) {
    437                 Err(err) => tracing::error!("Error executing post action: {err}"),
    438                 Ok(_) => tracing::debug!("Post action executed"),
    439             }
    440 
    441             Some(RouterAction::GoBack)
    442         }
    443         RenderNavAction::NoteAction(note_action) => {
    444             let txn = Transaction::new(ctx.ndb).expect("txn");
    445 
    446             crate::actionbar::execute_and_process_note_action(
    447                 note_action,
    448                 ctx.ndb,
    449                 get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache),
    450                 col,
    451                 &mut app.timeline_cache,
    452                 &mut app.threads,
    453                 ctx.note_cache,
    454                 ctx.pool,
    455                 &txn,
    456                 ctx.unknown_ids,
    457                 ctx.accounts,
    458                 ctx.global_wallet,
    459                 ctx.zaps,
    460                 ctx.img_cache,
    461                 &mut app.view_state,
    462                 ui,
    463             )
    464         }
    465         RenderNavAction::SwitchingAction(switching_action) => {
    466             if switching_action.process(
    467                 &mut app.timeline_cache,
    468                 &mut app.decks_cache,
    469                 ctx,
    470                 ui.ctx(),
    471             ) {
    472                 return Some(ProcessNavResult::SwitchOccurred);
    473             } else {
    474                 return None;
    475             }
    476         }
    477         RenderNavAction::ProfileAction(profile_action) => {
    478             profile_action.process_profile_action(ctx.ndb, ctx.pool, ctx.accounts)
    479         }
    480         RenderNavAction::WalletAction(wallet_action) => {
    481             wallet_action.process(ctx.accounts, ctx.global_wallet)
    482         }
    483         RenderNavAction::RelayAction(action) => {
    484             ctx.accounts
    485                 .process_relay_action(ui.ctx(), ctx.pool, action);
    486             None
    487         }
    488         RenderNavAction::SettingsAction(action) => {
    489             action.process_settings_action(app, ctx.settings, ctx.i18n, ctx.img_cache, ui.ctx())
    490         }
    491     };
    492 
    493     if let Some(action) = router_action {
    494         let cols =
    495             get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache).column_mut(col);
    496         let router = &mut cols.router;
    497         let sheet_router = &mut cols.sheet_router;
    498 
    499         action.process(router, sheet_router)
    500     } else {
    501         None
    502     }
    503 }
    504 
    505 fn render_nav_body(
    506     ui: &mut egui::Ui,
    507     app: &mut Damus,
    508     ctx: &mut AppContext,
    509     top: &Route,
    510     depth: usize,
    511     col: usize,
    512     inner_rect: egui::Rect,
    513 ) -> Option<RenderNavAction> {
    514     let mut note_context = NoteContext {
    515         ndb: ctx.ndb,
    516         accounts: ctx.accounts,
    517         img_cache: ctx.img_cache,
    518         note_cache: ctx.note_cache,
    519         zaps: ctx.zaps,
    520         pool: ctx.pool,
    521         job_pool: ctx.job_pool,
    522         unknown_ids: ctx.unknown_ids,
    523         clipboard: ctx.clipboard,
    524         i18n: ctx.i18n,
    525         global_wallet: ctx.global_wallet,
    526     };
    527     match top {
    528         Route::Timeline(kind) => {
    529             // did something request scroll to top for the selection column?
    530             let scroll_to_top = app
    531                 .decks_cache
    532                 .selected_column_index(ctx.accounts)
    533                 .is_some_and(|ind| ind == col)
    534                 && app.options.contains(AppOptions::ScrollToTop);
    535 
    536             let nav_action = render_timeline_route(
    537                 &mut app.timeline_cache,
    538                 kind,
    539                 col,
    540                 app.note_options,
    541                 depth,
    542                 ui,
    543                 &mut note_context,
    544                 &mut app.jobs,
    545                 scroll_to_top,
    546             );
    547 
    548             app.timeline_cache.set_fresh(kind);
    549 
    550             // always clear the scroll_to_top request
    551             if scroll_to_top {
    552                 app.options.remove(AppOptions::ScrollToTop);
    553             }
    554 
    555             nav_action
    556         }
    557         Route::Thread(selection) => render_thread_route(
    558             &mut app.threads,
    559             selection,
    560             col,
    561             app.note_options,
    562             ui,
    563             &mut note_context,
    564             &mut app.jobs,
    565         ),
    566         Route::Accounts(amr) => {
    567             let mut action = render_accounts_route(
    568                 ui,
    569                 ctx,
    570                 col,
    571                 &mut app.decks_cache,
    572                 &mut app.timeline_cache,
    573                 &mut app.view_state.login,
    574                 *amr,
    575             );
    576             let txn = Transaction::new(ctx.ndb).expect("txn");
    577             action.process_action(ctx.unknown_ids, ctx.ndb, &txn);
    578             action
    579                 .accounts_action
    580                 .map(|f| RenderNavAction::SwitchingAction(SwitchingAction::Accounts(f)))
    581         }
    582         Route::Relays => RelayView::new(ctx.pool, &mut app.view_state.id_string_map, ctx.i18n)
    583             .ui(ui)
    584             .map(RenderNavAction::RelayAction),
    585 
    586         Route::Settings => SettingsView::new(
    587             ctx.settings.get_settings_mut(),
    588             &mut note_context,
    589             &mut app.note_options,
    590             &mut app.jobs,
    591         )
    592         .ui(ui)
    593         .map(RenderNavAction::SettingsAction),
    594 
    595         Route::Reply(id) => {
    596             let txn = if let Ok(txn) = Transaction::new(ctx.ndb) {
    597                 txn
    598             } else {
    599                 ui.label(tr!(
    600                     note_context.i18n,
    601                     "Reply to unknown note",
    602                     "Error message when reply note cannot be found"
    603                 ));
    604                 return None;
    605             };
    606 
    607             let note = if let Ok(note) = ctx.ndb.get_note_by_id(&txn, id.bytes()) {
    608                 note
    609             } else {
    610                 ui.label(tr!(
    611                     note_context.i18n,
    612                     "Reply to unknown note",
    613                     "Error message when reply note cannot be found"
    614                 ));
    615                 return None;
    616             };
    617 
    618             let poster = ctx.accounts.selected_filled()?;
    619 
    620             let action = {
    621                 let draft = app.drafts.reply_mut(note.id());
    622 
    623                 let response = ui::PostReplyView::new(
    624                     &mut note_context,
    625                     poster,
    626                     draft,
    627                     &note,
    628                     inner_rect,
    629                     app.note_options,
    630                     &mut app.jobs,
    631                     col,
    632                 )
    633                 .show(ui);
    634 
    635                 response.action
    636             };
    637 
    638             action.map(Into::into)
    639         }
    640         Route::Quote(id) => {
    641             let txn = Transaction::new(ctx.ndb).expect("txn");
    642 
    643             let note = if let Ok(note) = ctx.ndb.get_note_by_id(&txn, id.bytes()) {
    644                 note
    645             } else {
    646                 ui.label(tr!(
    647                     note_context.i18n,
    648                     "Quote of unknown note",
    649                     "Error message when quote note cannot be found"
    650                 ));
    651                 return None;
    652             };
    653 
    654             let poster = ctx.accounts.selected_filled()?;
    655             let draft = app.drafts.quote_mut(note.id());
    656 
    657             let response = crate::ui::note::QuoteRepostView::new(
    658                 &mut note_context,
    659                 poster,
    660                 draft,
    661                 &note,
    662                 inner_rect,
    663                 app.note_options,
    664                 &mut app.jobs,
    665                 col,
    666             )
    667             .show(ui);
    668 
    669             response.action.map(Into::into)
    670         }
    671         Route::ComposeNote => {
    672             let kp = ctx.accounts.get_selected_account().key.to_full()?;
    673             let draft = app.drafts.compose_mut();
    674 
    675             let txn = Transaction::new(ctx.ndb).expect("txn");
    676             let post_response = ui::PostView::new(
    677                 &mut note_context,
    678                 draft,
    679                 PostType::New,
    680                 kp,
    681                 inner_rect,
    682                 app.note_options,
    683                 &mut app.jobs,
    684             )
    685             .ui(&txn, ui);
    686 
    687             post_response.action.map(Into::into)
    688         }
    689         Route::AddColumn(route) => {
    690             render_add_column_routes(ui, app, ctx, col, route);
    691 
    692             None
    693         }
    694         Route::Support => {
    695             SupportView::new(&mut app.support, ctx.i18n).show(ui);
    696             None
    697         }
    698         Route::Search => {
    699             let id = ui.id().with(("search", depth, col));
    700             let navigating =
    701                 get_active_columns_mut(note_context.i18n, ctx.accounts, &mut app.decks_cache)
    702                     .column(col)
    703                     .router()
    704                     .navigating;
    705             let search_buffer = app.view_state.searches.entry(id).or_default();
    706             let txn = Transaction::new(ctx.ndb).expect("txn");
    707 
    708             if navigating {
    709                 search_buffer.focus_state = FocusState::Navigating
    710             } else if search_buffer.focus_state == FocusState::Navigating {
    711                 // we're not navigating but our last search buffer state
    712                 // says we were navigating. This means that navigating has
    713                 // stopped. Let's make sure to focus the input field
    714                 search_buffer.focus_state = FocusState::ShouldRequestFocus;
    715             }
    716 
    717             SearchView::new(
    718                 &txn,
    719                 app.note_options,
    720                 search_buffer,
    721                 &mut note_context,
    722                 &mut app.jobs,
    723             )
    724             .show(ui)
    725             .map(RenderNavAction::NoteAction)
    726         }
    727         Route::NewDeck => {
    728             let id = ui.id().with("new-deck");
    729             let new_deck_state = app.view_state.id_to_deck_state.entry(id).or_default();
    730             let mut resp = None;
    731             if let Some(config_resp) = ConfigureDeckView::new(new_deck_state, ctx.i18n).ui(ui) {
    732                 let cur_acc = ctx.accounts.selected_account_pubkey();
    733                 app.decks_cache
    734                     .add_deck(*cur_acc, Deck::new(config_resp.icon, config_resp.name));
    735 
    736                 // set new deck as active
    737                 let cur_index = get_decks_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
    738                     .decks()
    739                     .len()
    740                     - 1;
    741                 resp = Some(RenderNavAction::SwitchingAction(SwitchingAction::Decks(
    742                     DecksAction::Switch(cur_index),
    743                 )));
    744 
    745                 new_deck_state.clear();
    746                 get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
    747                     .get_selected_router()
    748                     .go_back();
    749             }
    750             resp
    751         }
    752         Route::EditDeck(index) => {
    753             let mut action = None;
    754             let cur_deck = get_decks_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
    755                 .decks_mut()
    756                 .get_mut(*index)
    757                 .expect("index wasn't valid");
    758             let id = ui
    759                 .id()
    760                 .with(("edit-deck", ctx.accounts.selected_account_pubkey(), index));
    761             let deck_state = app
    762                 .view_state
    763                 .id_to_deck_state
    764                 .entry(id)
    765                 .or_insert_with(|| DeckState::from_deck(cur_deck));
    766             if let Some(resp) = EditDeckView::new(deck_state, ctx.i18n).ui(ui) {
    767                 match resp {
    768                     EditDeckResponse::Edit(configure_deck_response) => {
    769                         cur_deck.edit(configure_deck_response);
    770                     }
    771                     EditDeckResponse::Delete => {
    772                         action = Some(RenderNavAction::SwitchingAction(SwitchingAction::Decks(
    773                             DecksAction::Removing(*index),
    774                         )));
    775                     }
    776                 }
    777                 get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
    778                     .get_selected_router()
    779                     .go_back();
    780             }
    781 
    782             action
    783         }
    784         Route::EditProfile(pubkey) => {
    785             let mut action = None;
    786             let Some(kp) = ctx.accounts.get_full(pubkey) else {
    787                 error!("Pubkey in EditProfile route did not have an nsec attached in Accounts");
    788                 return None;
    789             };
    790 
    791             let Some(state) = app.view_state.pubkey_to_profile_state.get_mut(kp.pubkey) else {
    792                 tracing::error!(
    793                     "No profile state when navigating to EditProfile... was handle_navigating_edit_profile not called?"
    794                 );
    795                 return action;
    796             };
    797 
    798             if EditProfileView::new(ctx.i18n, state, ctx.img_cache).ui(ui) {
    799                 if let Some(state) = app.view_state.pubkey_to_profile_state.get(kp.pubkey) {
    800                     action = Some(RenderNavAction::ProfileAction(ProfileAction::SaveChanges(
    801                         SaveProfileChanges::new(kp.to_full(), state.clone()),
    802                     )))
    803                 }
    804             }
    805 
    806             action
    807         }
    808         Route::Wallet(wallet_type) => {
    809             let state = match wallet_type {
    810                 notedeck::WalletType::Auto => 's: {
    811                     if let Some(cur_acc_wallet) = ctx.accounts.get_selected_wallet_mut() {
    812                         let default_zap_state =
    813                             get_default_zap_state(&mut cur_acc_wallet.default_zap);
    814                         break 's WalletState::Wallet {
    815                             wallet: &mut cur_acc_wallet.wallet,
    816                             default_zap_state,
    817                             can_create_local_wallet: false,
    818                         };
    819                     }
    820 
    821                     let Some(wallet) = &mut ctx.global_wallet.wallet else {
    822                         break 's WalletState::NoWallet {
    823                             state: &mut ctx.global_wallet.ui_state,
    824                             show_local_only: true,
    825                         };
    826                     };
    827 
    828                     let default_zap_state = get_default_zap_state(&mut wallet.default_zap);
    829                     WalletState::Wallet {
    830                         wallet: &mut wallet.wallet,
    831                         default_zap_state,
    832                         can_create_local_wallet: true,
    833                     }
    834                 }
    835                 notedeck::WalletType::Local => 's: {
    836                     let cur_acc = ctx.accounts.get_selected_wallet_mut();
    837                     let Some(wallet) = cur_acc else {
    838                         break 's WalletState::NoWallet {
    839                             state: &mut ctx.global_wallet.ui_state,
    840                             show_local_only: false,
    841                         };
    842                     };
    843 
    844                     let default_zap_state = get_default_zap_state(&mut wallet.default_zap);
    845                     WalletState::Wallet {
    846                         wallet: &mut wallet.wallet,
    847                         default_zap_state,
    848                         can_create_local_wallet: false,
    849                     }
    850                 }
    851             };
    852 
    853             WalletView::new(state, ctx.i18n, ctx.clipboard)
    854                 .ui(ui)
    855                 .map(RenderNavAction::WalletAction)
    856         }
    857         Route::CustomizeZapAmount(target) => {
    858             let txn = Transaction::new(ctx.ndb).expect("txn");
    859             let default_msats = get_current_default_msats(ctx.accounts, ctx.global_wallet);
    860             CustomZapView::new(
    861                 ctx.i18n,
    862                 ctx.img_cache,
    863                 ctx.ndb,
    864                 &txn,
    865                 &target.zap_recipient,
    866                 default_msats,
    867             )
    868             .ui(ui)
    869             .map(|msats| {
    870                 get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
    871                     .column_mut(col)
    872                     .router_mut()
    873                     .go_back();
    874                 RenderNavAction::NoteAction(NoteAction::Zap(notedeck::ZapAction::Send(
    875                     notedeck::note::ZapTargetAmount {
    876                         target: target.clone(),
    877                         specified_msats: Some(msats),
    878                     },
    879                 )))
    880             })
    881         }
    882     }
    883 }
    884 
    885 #[must_use = "RenderNavResponse must be handled by calling .process_render_nav_response(..)"]
    886 pub fn render_nav(
    887     col: usize,
    888     inner_rect: egui::Rect,
    889     app: &mut Damus,
    890     ctx: &mut AppContext<'_>,
    891     ui: &mut egui::Ui,
    892 ) -> RenderNavResponse {
    893     let narrow = is_narrow(ui.ctx());
    894 
    895     if let Some(sheet_route) = app
    896         .columns(ctx.accounts)
    897         .column(col)
    898         .sheet_router
    899         .route()
    900         .clone()
    901     {
    902         let navigating = app
    903             .columns(ctx.accounts)
    904             .column(col)
    905             .sheet_router
    906             .navigating;
    907         let returning = app.columns(ctx.accounts).column(col).sheet_router.returning;
    908         let bg_route = app
    909             .columns(ctx.accounts)
    910             .column(col)
    911             .router()
    912             .routes()
    913             .last()
    914             .cloned();
    915         if let Some(bg_route) = bg_route {
    916             let resp = PopupSheet::new(&bg_route, &sheet_route)
    917                 .id_source(egui::Id::new(("nav", col)))
    918                 .navigating(navigating)
    919                 .returning(returning)
    920                 .with_split_percent_from_top(Percent::new(35).expect("35 <= 100"))
    921                 .show_mut(ui, |ui, typ, route| match typ {
    922                     NavUiType::Title => NavTitle::new(
    923                         ctx.ndb,
    924                         ctx.img_cache,
    925                         get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache),
    926                         &[route.clone()],
    927                         col,
    928                         ctx.i18n,
    929                     )
    930                     .show_move_button(!narrow)
    931                     .show_delete_button(!narrow)
    932                     .show(ui),
    933                     NavUiType::Body => render_nav_body(ui, app, ctx, route, 1, col, inner_rect),
    934                 });
    935 
    936             return RenderNavResponse::new(col, NotedeckNavResponse::Popup(Box::new(resp)));
    937         }
    938     };
    939 
    940     let routes = app
    941         .columns(ctx.accounts)
    942         .column(col)
    943         .router()
    944         .routes()
    945         .clone();
    946     let nav = Nav::new(&routes).id_source(egui::Id::new(("nav", col)));
    947 
    948     let drag_ids = 's: {
    949         let Some(top_route) = &routes.last().cloned() else {
    950             break 's None;
    951         };
    952 
    953         let Some(scroll_id) = get_scroll_id(
    954             top_route,
    955             app.columns(ctx.accounts)
    956                 .column(col)
    957                 .router()
    958                 .routes()
    959                 .len(),
    960             &app.timeline_cache,
    961             col,
    962         ) else {
    963             break 's None;
    964         };
    965 
    966         let vertical_drag_id = if route_uses_frame(top_route) {
    967             get_drag_id_through_frame(ui, scroll_id)
    968         } else {
    969             get_drag_id(ui, scroll_id)
    970         };
    971 
    972         let horizontal_drag_id = nav.drag_id(ui);
    973 
    974         let drag = &mut get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
    975             .column_mut(col)
    976             .drag;
    977 
    978         drag.update(horizontal_drag_id, vertical_drag_id, ui.ctx());
    979 
    980         Some((horizontal_drag_id, vertical_drag_id))
    981     };
    982 
    983     let nav_response = nav
    984         .navigating(
    985             app.columns_mut(ctx.i18n, ctx.accounts)
    986                 .column_mut(col)
    987                 .router_mut()
    988                 .navigating,
    989         )
    990         .returning(
    991             app.columns_mut(ctx.i18n, ctx.accounts)
    992                 .column_mut(col)
    993                 .router_mut()
    994                 .returning,
    995         )
    996         .show_mut(ui, |ui, render_type, nav| match render_type {
    997             NavUiType::Title => NavTitle::new(
    998                 ctx.ndb,
    999                 ctx.img_cache,
   1000                 get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache),
   1001                 nav.routes(),
   1002                 col,
   1003                 ctx.i18n,
   1004             )
   1005             .show_move_button(!narrow)
   1006             .show_delete_button(!narrow)
   1007             .show(ui),
   1008 
   1009             NavUiType::Body => {
   1010                 if let Some(top) = nav.routes().last() {
   1011                     render_nav_body(ui, app, ctx, top, nav.routes().len(), col, inner_rect)
   1012                 } else {
   1013                     None
   1014                 }
   1015             }
   1016         });
   1017 
   1018     if let Some((horizontal_drag_id, vertical_drag_id)) = drag_ids {
   1019         let drag = &mut get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
   1020             .column_mut(col)
   1021             .drag;
   1022         drag.check_for_drag_start(ui.ctx(), horizontal_drag_id, vertical_drag_id);
   1023     }
   1024 
   1025     RenderNavResponse::new(col, NotedeckNavResponse::Nav(Box::new(nav_response)))
   1026 }
   1027 
   1028 fn get_scroll_id(
   1029     top: &Route,
   1030     depth: usize,
   1031     timeline_cache: &TimelineCache,
   1032     col: usize,
   1033 ) -> Option<egui::Id> {
   1034     match top {
   1035         Route::Timeline(timeline_kind) => match timeline_kind {
   1036             TimelineKind::List(_)
   1037             | TimelineKind::Search(_)
   1038             | TimelineKind::Algo(_)
   1039             | TimelineKind::Notifications(_)
   1040             | TimelineKind::Universe
   1041             | TimelineKind::Hashtag(_)
   1042             | TimelineKind::Generic(_) => {
   1043                 TimelineView::scroll_id(timeline_cache, timeline_kind, col)
   1044             }
   1045             TimelineKind::Profile(pubkey) => {
   1046                 if depth > 1 {
   1047                     Some(ProfileView::scroll_id(col, pubkey))
   1048                 } else {
   1049                     TimelineView::scroll_id(timeline_cache, timeline_kind, col)
   1050                 }
   1051             }
   1052         },
   1053         Route::Thread(thread_selection) => Some(ThreadView::scroll_id(
   1054             thread_selection.selected_or_root(),
   1055             col,
   1056         )),
   1057         Route::Accounts(accounts_route) => match accounts_route {
   1058             crate::accounts::AccountsRoute::Accounts => Some(AccountsView::scroll_id()),
   1059             crate::accounts::AccountsRoute::AddAccount => None,
   1060         },
   1061         Route::Reply(note_id) => Some(PostReplyView::scroll_id(col, note_id.bytes())),
   1062         Route::Quote(note_id) => Some(QuoteRepostView::scroll_id(col, note_id.bytes())),
   1063         Route::Relays => Some(RelayView::scroll_id()),
   1064         Route::ComposeNote => Some(PostView::scroll_id()),
   1065         Route::AddColumn(add_column_route) => Some(AddColumnView::scroll_id(add_column_route)),
   1066         Route::EditProfile(_) => Some(EditProfileView::scroll_id()),
   1067         Route::Support => None,
   1068         Route::NewDeck => Some(ConfigureDeckView::scroll_id()),
   1069         Route::Search => Some(SearchView::scroll_id()),
   1070         Route::EditDeck(_) => None,
   1071         Route::Wallet(_) => None,
   1072         Route::CustomizeZapAmount(_) => None,
   1073         Route::Settings => None,
   1074     }
   1075 }
   1076 
   1077 /// Does the corresponding View for the route use a egui::Frame to wrap the ScrollArea?
   1078 /// TODO(kernelkind): this is quite hacky...
   1079 fn route_uses_frame(route: &Route) -> bool {
   1080     match route {
   1081         Route::Accounts(accounts_route) => match accounts_route {
   1082             crate::accounts::AccountsRoute::Accounts => true,
   1083             crate::accounts::AccountsRoute::AddAccount => false,
   1084         },
   1085         Route::Relays => true,
   1086         Route::Timeline(_) => false,
   1087         Route::Thread(_) => false,
   1088         Route::Reply(_) => false,
   1089         Route::Quote(_) => false,
   1090         Route::Settings => false,
   1091         Route::ComposeNote => false,
   1092         Route::AddColumn(_) => false,
   1093         Route::EditProfile(_) => false,
   1094         Route::Support => false,
   1095         Route::NewDeck => false,
   1096         Route::Search => false,
   1097         Route::EditDeck(_) => false,
   1098         Route::Wallet(_) => false,
   1099         Route::CustomizeZapAmount(_) => false,
   1100     }
   1101 }