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