notedeck

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

nav.rs (42680B)


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