nav.rs (18599B)
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::{PostAction, PostType}, 20 profile::EditProfileView, 21 search::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 match top { 248 Route::Timeline(kind) => render_timeline_route( 249 ctx.ndb, 250 ctx.img_cache, 251 ctx.unknown_ids, 252 ctx.note_cache, 253 &mut app.timeline_cache, 254 ctx.accounts, 255 kind, 256 col, 257 app.note_options, 258 depth, 259 ui, 260 ), 261 262 Route::Accounts(amr) => { 263 let mut action = render_accounts_route( 264 ui, 265 ctx.ndb, 266 col, 267 ctx.img_cache, 268 ctx.accounts, 269 &mut app.decks_cache, 270 &mut app.view_state.login, 271 *amr, 272 ); 273 let txn = Transaction::new(ctx.ndb).expect("txn"); 274 action.process_action(ctx.unknown_ids, ctx.ndb, &txn); 275 action 276 .accounts_action 277 .map(|f| RenderNavAction::SwitchingAction(SwitchingAction::Accounts(f))) 278 } 279 280 Route::Relays => { 281 let manager = RelayPoolManager::new(ctx.pool); 282 RelayView::new(ctx.accounts, manager, &mut app.view_state.id_string_map).ui(ui); 283 None 284 } 285 286 Route::Reply(id) => { 287 let txn = if let Ok(txn) = Transaction::new(ctx.ndb) { 288 txn 289 } else { 290 ui.label("Reply to unknown note"); 291 return None; 292 }; 293 294 let note = if let Ok(note) = ctx.ndb.get_note_by_id(&txn, id.bytes()) { 295 note 296 } else { 297 ui.label("Reply to unknown note"); 298 return None; 299 }; 300 301 let id = egui::Id::new(("post", col, note.key().unwrap())); 302 let poster = ctx.accounts.selected_or_first_nsec()?; 303 304 let action = { 305 let draft = app.drafts.reply_mut(note.id()); 306 307 let response = egui::ScrollArea::vertical().show(ui, |ui| { 308 ui::PostReplyView::new( 309 ctx.ndb, 310 poster, 311 draft, 312 ctx.note_cache, 313 ctx.img_cache, 314 ¬e, 315 inner_rect, 316 app.note_options, 317 ) 318 .id_source(id) 319 .show(ui) 320 }); 321 322 response.inner.action 323 }; 324 325 action.map(Into::into) 326 } 327 328 Route::Quote(id) => { 329 let txn = Transaction::new(ctx.ndb).expect("txn"); 330 331 let note = if let Ok(note) = ctx.ndb.get_note_by_id(&txn, id.bytes()) { 332 note 333 } else { 334 ui.label("Quote of unknown note"); 335 return None; 336 }; 337 338 let id = egui::Id::new(("post", col, note.key().unwrap())); 339 340 let poster = ctx.accounts.selected_or_first_nsec()?; 341 let draft = app.drafts.quote_mut(note.id()); 342 343 let response = egui::ScrollArea::vertical().show(ui, |ui| { 344 crate::ui::note::QuoteRepostView::new( 345 ctx.ndb, 346 poster, 347 ctx.note_cache, 348 ctx.img_cache, 349 draft, 350 ¬e, 351 inner_rect, 352 app.note_options, 353 ) 354 .id_source(id) 355 .show(ui) 356 }); 357 358 response.inner.action.map(Into::into) 359 } 360 361 Route::ComposeNote => { 362 let kp = ctx.accounts.get_selected_account()?.to_full()?; 363 let draft = app.drafts.compose_mut(); 364 365 let txn = Transaction::new(ctx.ndb).expect("txn"); 366 let post_response = ui::PostView::new( 367 ctx.ndb, 368 draft, 369 PostType::New, 370 ctx.img_cache, 371 ctx.note_cache, 372 kp, 373 inner_rect, 374 app.note_options, 375 ) 376 .ui(&txn, ui); 377 378 post_response.action.map(Into::into) 379 } 380 381 Route::AddColumn(route) => { 382 render_add_column_routes(ui, app, ctx, col, route); 383 384 None 385 } 386 387 Route::Support => { 388 SupportView::new(&mut app.support).show(ui); 389 None 390 } 391 392 Route::Search => { 393 let id = ui.id().with(("search", depth, col)); 394 let search_buffer = app.view_state.searches.entry(id).or_default(); 395 let txn = Transaction::new(ctx.ndb).expect("txn"); 396 397 SearchView::new( 398 ctx.ndb, 399 &txn, 400 ctx.note_cache, 401 ctx.img_cache, 402 &ctx.accounts.mutefun(), 403 app.note_options, 404 search_buffer, 405 ) 406 .show(ui) 407 .map(RenderNavAction::NoteAction) 408 } 409 410 Route::NewDeck => { 411 let id = ui.id().with("new-deck"); 412 let new_deck_state = app.view_state.id_to_deck_state.entry(id).or_default(); 413 let mut resp = None; 414 if let Some(config_resp) = ConfigureDeckView::new(new_deck_state).ui(ui) { 415 if let Some(cur_acc) = ctx.accounts.get_selected_account() { 416 app.decks_cache.add_deck( 417 cur_acc.pubkey, 418 Deck::new(config_resp.icon, config_resp.name), 419 ); 420 421 // set new deck as active 422 let cur_index = get_decks_mut(ctx.accounts, &mut app.decks_cache) 423 .decks() 424 .len() 425 - 1; 426 resp = Some(RenderNavAction::SwitchingAction(SwitchingAction::Decks( 427 DecksAction::Switch(cur_index), 428 ))); 429 } 430 431 new_deck_state.clear(); 432 get_active_columns_mut(ctx.accounts, &mut app.decks_cache) 433 .get_first_router() 434 .go_back(); 435 } 436 resp 437 } 438 Route::EditDeck(index) => { 439 let mut action = None; 440 let cur_deck = get_decks_mut(ctx.accounts, &mut app.decks_cache) 441 .decks_mut() 442 .get_mut(*index) 443 .expect("index wasn't valid"); 444 let id = ui.id().with(( 445 "edit-deck", 446 ctx.accounts.get_selected_account().map(|k| k.pubkey), 447 index, 448 )); 449 let deck_state = app 450 .view_state 451 .id_to_deck_state 452 .entry(id) 453 .or_insert_with(|| DeckState::from_deck(cur_deck)); 454 if let Some(resp) = EditDeckView::new(deck_state).ui(ui) { 455 match resp { 456 EditDeckResponse::Edit(configure_deck_response) => { 457 cur_deck.edit(configure_deck_response); 458 } 459 EditDeckResponse::Delete => { 460 action = Some(RenderNavAction::SwitchingAction(SwitchingAction::Decks( 461 DecksAction::Removing(*index), 462 ))); 463 } 464 } 465 get_active_columns_mut(ctx.accounts, &mut app.decks_cache) 466 .get_first_router() 467 .go_back(); 468 } 469 470 action 471 } 472 Route::EditProfile(pubkey) => { 473 let mut action = None; 474 if let Some(kp) = ctx.accounts.get_full(pubkey.bytes()) { 475 let state = app 476 .view_state 477 .pubkey_to_profile_state 478 .entry(*kp.pubkey) 479 .or_insert_with(|| { 480 let txn = Transaction::new(ctx.ndb).expect("txn"); 481 if let Ok(record) = ctx.ndb.get_profile_by_pubkey(&txn, kp.pubkey.bytes()) { 482 ProfileState::from_profile(&record) 483 } else { 484 ProfileState::default() 485 } 486 }); 487 if EditProfileView::new(state, ctx.img_cache).ui(ui) { 488 if let Some(taken_state) = 489 app.view_state.pubkey_to_profile_state.remove(kp.pubkey) 490 { 491 action = Some(RenderNavAction::ProfileAction(ProfileAction::SaveChanges( 492 SaveProfileChanges::new(kp.to_full(), taken_state), 493 ))) 494 } 495 } 496 } else { 497 error!("Pubkey in EditProfile route did not have an nsec attached in Accounts"); 498 } 499 action 500 } 501 } 502 } 503 504 #[must_use = "RenderNavResponse must be handled by calling .process_render_nav_response(..)"] 505 pub fn render_nav( 506 col: usize, 507 inner_rect: egui::Rect, 508 app: &mut Damus, 509 ctx: &mut AppContext<'_>, 510 ui: &mut egui::Ui, 511 ) -> RenderNavResponse { 512 let nav_response = Nav::new( 513 &app.columns(ctx.accounts) 514 .column(col) 515 .router() 516 .routes() 517 .clone(), 518 ) 519 .navigating( 520 app.columns_mut(ctx.accounts) 521 .column_mut(col) 522 .router_mut() 523 .navigating, 524 ) 525 .returning( 526 app.columns_mut(ctx.accounts) 527 .column_mut(col) 528 .router_mut() 529 .returning, 530 ) 531 .id_source(egui::Id::new(("nav", col))) 532 .show_mut(ui, |ui, render_type, nav| match render_type { 533 NavUiType::Title => NavTitle::new( 534 ctx.ndb, 535 ctx.img_cache, 536 get_active_columns_mut(ctx.accounts, &mut app.decks_cache), 537 nav.routes(), 538 col, 539 ) 540 .show(ui), 541 NavUiType::Body => { 542 if let Some(top) = nav.routes().last() { 543 render_nav_body(ui, app, ctx, top, nav.routes().len(), col, inner_rect) 544 } else { 545 None 546 } 547 } 548 }); 549 550 RenderNavResponse::new(col, nav_response) 551 }