notedeck

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

nav.rs (46574B)


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