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