nav.rs (19564B)
1 use crate::{ 2 accounts::render_accounts_route, 3 actionbar::NoteAction, 4 app::{get_active_columns_mut, get_decks_mut}, 5 column::ColumnsAction, 6 deck_state::DeckState, 7 decks::{Deck, DecksAction, DecksCache}, 8 profile::{ProfileAction, SaveProfileChanges}, 9 profile_state::ProfileState, 10 relay_pool_manager::RelayPoolManager, 11 route::Route, 12 timeline::{route::render_timeline_route, TimelineCache}, 13 ui::{ 14 self, 15 add_column::render_add_column_routes, 16 column::NavTitle, 17 configure_deck::ConfigureDeckView, 18 edit_deck::{EditDeckResponse, EditDeckView}, 19 note::{contents::NoteContext, PostAction, PostType}, 20 profile::EditProfileView, 21 search::{FocusState, SearchView}, 22 support::SupportView, 23 RelayView, View, 24 }, 25 Damus, 26 }; 27 28 use egui_nav::{Nav, NavAction, NavResponse, NavUiType}; 29 use nostrdb::Transaction; 30 use notedeck::{AccountsAction, AppContext}; 31 use tracing::error; 32 33 #[allow(clippy::enum_variant_names)] 34 pub enum RenderNavAction { 35 Back, 36 RemoveColumn, 37 PostAction(PostAction), 38 NoteAction(NoteAction), 39 ProfileAction(ProfileAction), 40 SwitchingAction(SwitchingAction), 41 } 42 43 pub enum SwitchingAction { 44 Accounts(AccountsAction), 45 Columns(ColumnsAction), 46 Decks(crate::decks::DecksAction), 47 } 48 49 impl SwitchingAction { 50 /// process the action, and return whether switching occured 51 pub fn process( 52 &self, 53 timeline_cache: &mut TimelineCache, 54 decks_cache: &mut DecksCache, 55 ctx: &mut AppContext<'_>, 56 ) -> bool { 57 match &self { 58 SwitchingAction::Accounts(account_action) => match account_action { 59 AccountsAction::Switch(switch_action) => { 60 ctx.accounts.select_account(switch_action.switch_to); 61 // pop nav after switch 62 if let Some(src) = switch_action.source { 63 get_active_columns_mut(ctx.accounts, decks_cache) 64 .column_mut(src) 65 .router_mut() 66 .go_back(); 67 } 68 } 69 AccountsAction::Remove(index) => ctx.accounts.remove_account(*index), 70 }, 71 SwitchingAction::Columns(columns_action) => match *columns_action { 72 ColumnsAction::Remove(index) => { 73 let kinds_to_pop = 74 get_active_columns_mut(ctx.accounts, decks_cache).delete_column(index); 75 for kind in &kinds_to_pop { 76 if let Err(err) = timeline_cache.pop(kind, ctx.ndb, ctx.pool) { 77 error!("error popping timeline: {err}"); 78 } 79 } 80 } 81 82 ColumnsAction::Switch(from, to) => { 83 get_active_columns_mut(ctx.accounts, decks_cache).move_col(from, to); 84 } 85 }, 86 SwitchingAction::Decks(decks_action) => match *decks_action { 87 DecksAction::Switch(index) => { 88 get_decks_mut(ctx.accounts, decks_cache).set_active(index) 89 } 90 DecksAction::Removing(index) => { 91 get_decks_mut(ctx.accounts, decks_cache).remove_deck(index) 92 } 93 }, 94 } 95 true 96 } 97 } 98 99 impl From<PostAction> for RenderNavAction { 100 fn from(post_action: PostAction) -> Self { 101 Self::PostAction(post_action) 102 } 103 } 104 105 impl From<NoteAction> for RenderNavAction { 106 fn from(note_action: NoteAction) -> RenderNavAction { 107 Self::NoteAction(note_action) 108 } 109 } 110 111 pub type NotedeckNavResponse = NavResponse<Option<RenderNavAction>>; 112 113 pub struct RenderNavResponse { 114 column: usize, 115 response: NotedeckNavResponse, 116 } 117 118 impl RenderNavResponse { 119 #[allow(private_interfaces)] 120 pub fn new(column: usize, response: NotedeckNavResponse) -> Self { 121 RenderNavResponse { column, response } 122 } 123 124 #[must_use = "Make sure to save columns if result is true"] 125 pub fn process_render_nav_response(&self, app: &mut Damus, ctx: &mut AppContext<'_>) -> bool { 126 let mut switching_occured: bool = false; 127 let col = self.column; 128 129 if let Some(action) = self 130 .response 131 .response 132 .as_ref() 133 .or(self.response.title_response.as_ref()) 134 { 135 // start returning when we're finished posting 136 match action { 137 RenderNavAction::Back => { 138 app.columns_mut(ctx.accounts) 139 .column_mut(col) 140 .router_mut() 141 .go_back(); 142 } 143 144 RenderNavAction::RemoveColumn => { 145 let kinds_to_pop = app.columns_mut(ctx.accounts).delete_column(col); 146 147 for kind in &kinds_to_pop { 148 if let Err(err) = app.timeline_cache.pop(kind, ctx.ndb, ctx.pool) { 149 error!("error popping timeline: {err}"); 150 } 151 } 152 153 switching_occured = true; 154 } 155 156 RenderNavAction::PostAction(post_action) => { 157 let txn = Transaction::new(ctx.ndb).expect("txn"); 158 let _ = post_action.execute(ctx.ndb, &txn, ctx.pool, &mut app.drafts); 159 get_active_columns_mut(ctx.accounts, &mut app.decks_cache) 160 .column_mut(col) 161 .router_mut() 162 .go_back(); 163 } 164 165 RenderNavAction::NoteAction(note_action) => { 166 let txn = Transaction::new(ctx.ndb).expect("txn"); 167 168 note_action.execute_and_process_result( 169 ctx.ndb, 170 get_active_columns_mut(ctx.accounts, &mut app.decks_cache), 171 col, 172 &mut app.timeline_cache, 173 ctx.note_cache, 174 ctx.pool, 175 &txn, 176 ctx.unknown_ids, 177 ); 178 } 179 180 RenderNavAction::SwitchingAction(switching_action) => { 181 switching_occured = switching_action.process( 182 &mut app.timeline_cache, 183 &mut app.decks_cache, 184 ctx, 185 ); 186 } 187 RenderNavAction::ProfileAction(profile_action) => { 188 profile_action.process( 189 &mut app.view_state.pubkey_to_profile_state, 190 ctx.ndb, 191 ctx.pool, 192 get_active_columns_mut(ctx.accounts, &mut app.decks_cache) 193 .column_mut(col) 194 .router_mut(), 195 ); 196 } 197 } 198 } 199 200 if let Some(action) = self.response.action { 201 match action { 202 NavAction::Returned => { 203 let r = app 204 .columns_mut(ctx.accounts) 205 .column_mut(col) 206 .router_mut() 207 .pop(); 208 209 if let Some(Route::Timeline(kind)) = &r { 210 if let Err(err) = app.timeline_cache.pop(kind, ctx.ndb, ctx.pool) { 211 error!("popping timeline had an error: {err} for {:?}", kind); 212 } 213 }; 214 215 switching_occured = true; 216 } 217 218 NavAction::Navigated => { 219 let cur_router = app.columns_mut(ctx.accounts).column_mut(col).router_mut(); 220 cur_router.navigating = false; 221 if cur_router.is_replacing() { 222 cur_router.remove_previous_routes(); 223 } 224 switching_occured = true; 225 } 226 227 NavAction::Dragging => {} 228 NavAction::Returning => {} 229 NavAction::Resetting => {} 230 NavAction::Navigating => {} 231 } 232 } 233 234 switching_occured 235 } 236 } 237 238 fn render_nav_body( 239 ui: &mut egui::Ui, 240 app: &mut Damus, 241 ctx: &mut AppContext<'_>, 242 top: &Route, 243 depth: usize, 244 col: usize, 245 inner_rect: egui::Rect, 246 ) -> Option<RenderNavAction> { 247 let mut note_context = NoteContext { 248 ndb: ctx.ndb, 249 img_cache: ctx.img_cache, 250 note_cache: ctx.note_cache, 251 }; 252 match top { 253 Route::Timeline(kind) => render_timeline_route( 254 ctx.unknown_ids, 255 &mut app.timeline_cache, 256 ctx.accounts, 257 kind, 258 col, 259 app.note_options, 260 depth, 261 ui, 262 &mut note_context, 263 ), 264 265 Route::Accounts(amr) => { 266 let mut action = render_accounts_route( 267 ui, 268 ctx.ndb, 269 col, 270 ctx.img_cache, 271 ctx.accounts, 272 &mut app.decks_cache, 273 &mut app.view_state.login, 274 *amr, 275 ); 276 let txn = Transaction::new(ctx.ndb).expect("txn"); 277 action.process_action(ctx.unknown_ids, ctx.ndb, &txn); 278 action 279 .accounts_action 280 .map(|f| RenderNavAction::SwitchingAction(SwitchingAction::Accounts(f))) 281 } 282 283 Route::Relays => { 284 let manager = RelayPoolManager::new(ctx.pool); 285 RelayView::new(ctx.accounts, manager, &mut app.view_state.id_string_map).ui(ui); 286 None 287 } 288 289 Route::Reply(id) => { 290 let txn = if let Ok(txn) = Transaction::new(ctx.ndb) { 291 txn 292 } else { 293 ui.label("Reply to unknown note"); 294 return None; 295 }; 296 297 let note = if let Ok(note) = ctx.ndb.get_note_by_id(&txn, id.bytes()) { 298 note 299 } else { 300 ui.label("Reply to unknown note"); 301 return None; 302 }; 303 304 let id = egui::Id::new(("post", col, note.key().unwrap())); 305 let poster = ctx.accounts.selected_or_first_nsec()?; 306 307 let action = { 308 let draft = app.drafts.reply_mut(note.id()); 309 310 let response = egui::ScrollArea::vertical() 311 .show(ui, |ui| { 312 ui::PostReplyView::new( 313 &mut note_context, 314 poster, 315 draft, 316 ¬e, 317 inner_rect, 318 app.note_options, 319 ) 320 .id_source(id) 321 .show(ui) 322 }) 323 .inner; 324 325 if let Some(selection) = response.context_selection { 326 selection.process(ui, ¬e); 327 } 328 329 response.action 330 }; 331 332 action.map(Into::into) 333 } 334 335 Route::Quote(id) => { 336 let txn = Transaction::new(ctx.ndb).expect("txn"); 337 338 let note = if let Ok(note) = ctx.ndb.get_note_by_id(&txn, id.bytes()) { 339 note 340 } else { 341 ui.label("Quote of unknown note"); 342 return None; 343 }; 344 345 let id = egui::Id::new(("post", col, note.key().unwrap())); 346 347 let poster = ctx.accounts.selected_or_first_nsec()?; 348 let draft = app.drafts.quote_mut(note.id()); 349 350 let response = egui::ScrollArea::vertical() 351 .show(ui, |ui| { 352 crate::ui::note::QuoteRepostView::new( 353 &mut note_context, 354 poster, 355 draft, 356 ¬e, 357 inner_rect, 358 app.note_options, 359 ) 360 .id_source(id) 361 .show(ui) 362 }) 363 .inner; 364 365 if let Some(selection) = response.context_selection { 366 selection.process(ui, ¬e); 367 } 368 369 response.action.map(Into::into) 370 } 371 372 Route::ComposeNote => { 373 let kp = ctx.accounts.get_selected_account()?.to_full()?; 374 let draft = app.drafts.compose_mut(); 375 376 let txn = Transaction::new(ctx.ndb).expect("txn"); 377 let post_response = ui::PostView::new( 378 &mut note_context, 379 draft, 380 PostType::New, 381 kp, 382 inner_rect, 383 app.note_options, 384 ) 385 .ui(&txn, ui); 386 387 post_response.action.map(Into::into) 388 } 389 390 Route::AddColumn(route) => { 391 render_add_column_routes(ui, app, ctx, col, route); 392 393 None 394 } 395 396 Route::Support => { 397 SupportView::new(&mut app.support).show(ui); 398 None 399 } 400 401 Route::Search => { 402 let id = ui.id().with(("search", depth, col)); 403 let navigating = app 404 .columns_mut(ctx.accounts) 405 .column(col) 406 .router() 407 .navigating; 408 let search_buffer = app.view_state.searches.entry(id).or_default(); 409 let txn = Transaction::new(ctx.ndb).expect("txn"); 410 411 if navigating { 412 search_buffer.focus_state = FocusState::Navigating 413 } else if search_buffer.focus_state == FocusState::Navigating { 414 // we're not navigating but our last search buffer state 415 // says we were navigating. This means that navigating has 416 // stopped. Let's make sure to focus the input field 417 search_buffer.focus_state = FocusState::ShouldRequestFocus; 418 } 419 420 SearchView::new( 421 &txn, 422 &ctx.accounts.mutefun(), 423 app.note_options, 424 search_buffer, 425 &mut note_context, 426 ) 427 .show(ui) 428 .map(RenderNavAction::NoteAction) 429 } 430 431 Route::NewDeck => { 432 let id = ui.id().with("new-deck"); 433 let new_deck_state = app.view_state.id_to_deck_state.entry(id).or_default(); 434 let mut resp = None; 435 if let Some(config_resp) = ConfigureDeckView::new(new_deck_state).ui(ui) { 436 if let Some(cur_acc) = ctx.accounts.get_selected_account() { 437 app.decks_cache.add_deck( 438 cur_acc.pubkey, 439 Deck::new(config_resp.icon, config_resp.name), 440 ); 441 442 // set new deck as active 443 let cur_index = get_decks_mut(ctx.accounts, &mut app.decks_cache) 444 .decks() 445 .len() 446 - 1; 447 resp = Some(RenderNavAction::SwitchingAction(SwitchingAction::Decks( 448 DecksAction::Switch(cur_index), 449 ))); 450 } 451 452 new_deck_state.clear(); 453 get_active_columns_mut(ctx.accounts, &mut app.decks_cache) 454 .get_first_router() 455 .go_back(); 456 } 457 resp 458 } 459 Route::EditDeck(index) => { 460 let mut action = None; 461 let cur_deck = get_decks_mut(ctx.accounts, &mut app.decks_cache) 462 .decks_mut() 463 .get_mut(*index) 464 .expect("index wasn't valid"); 465 let id = ui.id().with(( 466 "edit-deck", 467 ctx.accounts.get_selected_account().map(|k| k.pubkey), 468 index, 469 )); 470 let deck_state = app 471 .view_state 472 .id_to_deck_state 473 .entry(id) 474 .or_insert_with(|| DeckState::from_deck(cur_deck)); 475 if let Some(resp) = EditDeckView::new(deck_state).ui(ui) { 476 match resp { 477 EditDeckResponse::Edit(configure_deck_response) => { 478 cur_deck.edit(configure_deck_response); 479 } 480 EditDeckResponse::Delete => { 481 action = Some(RenderNavAction::SwitchingAction(SwitchingAction::Decks( 482 DecksAction::Removing(*index), 483 ))); 484 } 485 } 486 get_active_columns_mut(ctx.accounts, &mut app.decks_cache) 487 .get_first_router() 488 .go_back(); 489 } 490 491 action 492 } 493 Route::EditProfile(pubkey) => { 494 let mut action = None; 495 if let Some(kp) = ctx.accounts.get_full(pubkey.bytes()) { 496 let state = app 497 .view_state 498 .pubkey_to_profile_state 499 .entry(*kp.pubkey) 500 .or_insert_with(|| { 501 let txn = Transaction::new(ctx.ndb).expect("txn"); 502 if let Ok(record) = ctx.ndb.get_profile_by_pubkey(&txn, kp.pubkey.bytes()) { 503 ProfileState::from_profile(&record) 504 } else { 505 ProfileState::default() 506 } 507 }); 508 if EditProfileView::new(state, ctx.img_cache).ui(ui) { 509 if let Some(taken_state) = 510 app.view_state.pubkey_to_profile_state.remove(kp.pubkey) 511 { 512 action = Some(RenderNavAction::ProfileAction(ProfileAction::SaveChanges( 513 SaveProfileChanges::new(kp.to_full(), taken_state), 514 ))) 515 } 516 } 517 } else { 518 error!("Pubkey in EditProfile route did not have an nsec attached in Accounts"); 519 } 520 action 521 } 522 } 523 } 524 525 #[must_use = "RenderNavResponse must be handled by calling .process_render_nav_response(..)"] 526 pub fn render_nav( 527 col: usize, 528 inner_rect: egui::Rect, 529 app: &mut Damus, 530 ctx: &mut AppContext<'_>, 531 ui: &mut egui::Ui, 532 ) -> RenderNavResponse { 533 let nav_response = Nav::new( 534 &app.columns(ctx.accounts) 535 .column(col) 536 .router() 537 .routes() 538 .clone(), 539 ) 540 .navigating( 541 app.columns_mut(ctx.accounts) 542 .column_mut(col) 543 .router_mut() 544 .navigating, 545 ) 546 .returning( 547 app.columns_mut(ctx.accounts) 548 .column_mut(col) 549 .router_mut() 550 .returning, 551 ) 552 .id_source(egui::Id::new(("nav", col))) 553 .show_mut(ui, |ui, render_type, nav| match render_type { 554 NavUiType::Title => NavTitle::new( 555 ctx.ndb, 556 ctx.img_cache, 557 get_active_columns_mut(ctx.accounts, &mut app.decks_cache), 558 nav.routes(), 559 col, 560 ) 561 .show(ui), 562 NavUiType::Body => { 563 if let Some(top) = nav.routes().last() { 564 render_nav_body(ui, app, ctx, top, nav.routes().len(), col, inner_rect) 565 } else { 566 None 567 } 568 } 569 }); 570 571 RenderNavResponse::new(col, nav_response) 572 }