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 ¬e, 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 ¬e, 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 }