nav.rs (17144B)
1 use crate::{ 2 accounts::render_accounts_route, 3 actionbar::NoteAction, 4 app::{get_active_columns, 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::{ 13 route::{render_timeline_route, TimelineRoute}, 14 Timeline, 15 }, 16 ui::{ 17 self, 18 add_column::render_add_column_routes, 19 column::NavTitle, 20 configure_deck::ConfigureDeckView, 21 edit_deck::{EditDeckResponse, EditDeckView}, 22 note::{PostAction, PostType}, 23 profile::EditProfileView, 24 support::SupportView, 25 RelayView, View, 26 }, 27 Damus, 28 }; 29 30 use notedeck::{AccountsAction, AppContext, RootIdError}; 31 32 use egui_nav::{Nav, NavAction, NavResponse, NavUiType}; 33 use nostrdb::{Ndb, Transaction}; 34 use tracing::{error, info}; 35 36 #[allow(clippy::enum_variant_names)] 37 pub enum RenderNavAction { 38 Back, 39 RemoveColumn, 40 PostAction(PostAction), 41 NoteAction(NoteAction), 42 ProfileAction(ProfileAction), 43 SwitchingAction(SwitchingAction), 44 } 45 46 pub enum SwitchingAction { 47 Accounts(AccountsAction), 48 Columns(ColumnsAction), 49 Decks(crate::decks::DecksAction), 50 } 51 52 impl SwitchingAction { 53 /// process the action, and return whether switching occured 54 pub fn process(&self, decks_cache: &mut DecksCache, ctx: &mut AppContext<'_>) -> bool { 55 match &self { 56 SwitchingAction::Accounts(account_action) => match account_action { 57 AccountsAction::Switch(switch_action) => { 58 ctx.accounts.select_account(switch_action.switch_to); 59 // pop nav after switch 60 if let Some(src) = switch_action.source { 61 get_active_columns_mut(ctx.accounts, decks_cache) 62 .column_mut(src) 63 .router_mut() 64 .go_back(); 65 } 66 } 67 AccountsAction::Remove(index) => ctx.accounts.remove_account(*index), 68 }, 69 SwitchingAction::Columns(columns_action) => match *columns_action { 70 ColumnsAction::Remove(index) => { 71 get_active_columns_mut(ctx.accounts, decks_cache).delete_column(index) 72 } 73 ColumnsAction::Switch(from, to) => { 74 get_active_columns_mut(ctx.accounts, decks_cache).move_col(from, to); 75 } 76 }, 77 SwitchingAction::Decks(decks_action) => match *decks_action { 78 DecksAction::Switch(index) => { 79 get_decks_mut(ctx.accounts, decks_cache).set_active(index) 80 } 81 DecksAction::Removing(index) => { 82 get_decks_mut(ctx.accounts, decks_cache).remove_deck(index) 83 } 84 }, 85 } 86 true 87 } 88 } 89 90 impl From<PostAction> for RenderNavAction { 91 fn from(post_action: PostAction) -> Self { 92 Self::PostAction(post_action) 93 } 94 } 95 96 impl From<NoteAction> for RenderNavAction { 97 fn from(note_action: NoteAction) -> RenderNavAction { 98 Self::NoteAction(note_action) 99 } 100 } 101 102 pub type NotedeckNavResponse = NavResponse<Option<RenderNavAction>>; 103 104 pub struct RenderNavResponse { 105 column: usize, 106 response: NotedeckNavResponse, 107 } 108 109 impl RenderNavResponse { 110 #[allow(private_interfaces)] 111 pub fn new(column: usize, response: NotedeckNavResponse) -> Self { 112 RenderNavResponse { column, response } 113 } 114 115 #[must_use = "Make sure to save columns if result is true"] 116 pub fn process_render_nav_response(&self, app: &mut Damus, ctx: &mut AppContext<'_>) -> bool { 117 let mut switching_occured: bool = false; 118 let col = self.column; 119 120 if let Some(action) = self 121 .response 122 .response 123 .as_ref() 124 .or(self.response.title_response.as_ref()) 125 { 126 // start returning when we're finished posting 127 match action { 128 RenderNavAction::Back => { 129 app.columns_mut(ctx.accounts) 130 .column_mut(col) 131 .router_mut() 132 .go_back(); 133 } 134 135 RenderNavAction::RemoveColumn => { 136 let tl = app 137 .columns(ctx.accounts) 138 .find_timeline_for_column_index(col); 139 if let Some(timeline) = tl { 140 unsubscribe_timeline(ctx.ndb, timeline); 141 } 142 143 app.columns_mut(ctx.accounts).delete_column(col); 144 switching_occured = true; 145 } 146 147 RenderNavAction::PostAction(post_action) => { 148 let txn = Transaction::new(ctx.ndb).expect("txn"); 149 let _ = post_action.execute(ctx.ndb, &txn, ctx.pool, &mut app.drafts); 150 get_active_columns_mut(ctx.accounts, &mut app.decks_cache) 151 .column_mut(col) 152 .router_mut() 153 .go_back(); 154 } 155 156 RenderNavAction::NoteAction(note_action) => { 157 let txn = Transaction::new(ctx.ndb).expect("txn"); 158 159 note_action.execute_and_process_result( 160 ctx.ndb, 161 get_active_columns_mut(ctx.accounts, &mut app.decks_cache), 162 col, 163 &mut app.timeline_cache, 164 ctx.note_cache, 165 ctx.pool, 166 &txn, 167 ctx.unknown_ids, 168 ); 169 } 170 171 RenderNavAction::SwitchingAction(switching_action) => { 172 switching_occured = switching_action.process(&mut app.decks_cache, ctx); 173 } 174 RenderNavAction::ProfileAction(profile_action) => { 175 profile_action.process( 176 &mut app.view_state.pubkey_to_profile_state, 177 ctx.ndb, 178 ctx.pool, 179 get_active_columns_mut(ctx.accounts, &mut app.decks_cache) 180 .column_mut(col) 181 .router_mut(), 182 ); 183 } 184 } 185 } 186 187 if let Some(action) = self.response.action { 188 match action { 189 NavAction::Returned => { 190 let r = app 191 .columns_mut(ctx.accounts) 192 .column_mut(col) 193 .router_mut() 194 .pop(); 195 let txn = Transaction::new(ctx.ndb).expect("txn"); 196 197 if let Some(Route::Timeline(TimelineRoute::Thread(id))) = r { 198 match notedeck::note::root_note_id_from_selected_id( 199 ctx.ndb, 200 ctx.note_cache, 201 &txn, 202 id.bytes(), 203 ) { 204 Ok(root_id) => { 205 if let Some(thread) = 206 app.timeline_cache.threads.get_mut(root_id.bytes()) 207 { 208 if let Some(sub) = &mut thread.subscription { 209 sub.unsubscribe(ctx.ndb, ctx.pool); 210 } 211 } 212 } 213 214 Err(RootIdError::NoteNotFound) => { 215 error!("thread returned: note not found for unsub??: {}", id.hex()) 216 } 217 218 Err(RootIdError::NoRootId) => { 219 error!("thread returned: note not found for unsub??: {}", id.hex()) 220 } 221 } 222 } else if let Some(Route::Timeline(TimelineRoute::Profile(pubkey))) = r { 223 if let Some(profile) = app.timeline_cache.profiles.get_mut(pubkey.bytes()) { 224 if let Some(sub) = &mut profile.subscription { 225 sub.unsubscribe(ctx.ndb, ctx.pool); 226 } 227 } 228 } 229 230 switching_occured = true; 231 } 232 233 NavAction::Navigated => { 234 let cur_router = app.columns_mut(ctx.accounts).column_mut(col).router_mut(); 235 cur_router.navigating = false; 236 if cur_router.is_replacing() { 237 cur_router.remove_previous_routes(); 238 } 239 switching_occured = true; 240 } 241 242 NavAction::Dragging => {} 243 NavAction::Returning => {} 244 NavAction::Resetting => {} 245 NavAction::Navigating => {} 246 } 247 } 248 249 switching_occured 250 } 251 } 252 253 fn render_nav_body( 254 ui: &mut egui::Ui, 255 app: &mut Damus, 256 ctx: &mut AppContext<'_>, 257 top: &Route, 258 col: usize, 259 ) -> Option<RenderNavAction> { 260 match top { 261 Route::Timeline(tlr) => render_timeline_route( 262 ctx.ndb, 263 get_active_columns_mut(ctx.accounts, &mut app.decks_cache), 264 &mut app.drafts, 265 ctx.img_cache, 266 ctx.unknown_ids, 267 ctx.note_cache, 268 &mut app.timeline_cache, 269 ctx.accounts, 270 *tlr, 271 col, 272 app.textmode, 273 ui, 274 ), 275 Route::Accounts(amr) => { 276 let mut action = render_accounts_route( 277 ui, 278 ctx.ndb, 279 col, 280 ctx.img_cache, 281 ctx.accounts, 282 &mut app.decks_cache, 283 &mut app.view_state.login, 284 *amr, 285 ); 286 let txn = Transaction::new(ctx.ndb).expect("txn"); 287 action.process_action(ctx.unknown_ids, ctx.ndb, &txn); 288 action 289 .accounts_action 290 .map(|f| RenderNavAction::SwitchingAction(SwitchingAction::Accounts(f))) 291 } 292 Route::Relays => { 293 let manager = RelayPoolManager::new(ctx.pool); 294 RelayView::new(ctx.accounts, manager, &mut app.view_state.id_string_map).ui(ui); 295 None 296 } 297 Route::ComposeNote => { 298 let kp = ctx.accounts.get_selected_account()?.to_full()?; 299 let draft = app.drafts.compose_mut(); 300 301 let txn = Transaction::new(ctx.ndb).expect("txn"); 302 let post_response = ui::PostView::new( 303 ctx.ndb, 304 draft, 305 PostType::New, 306 ctx.img_cache, 307 ctx.note_cache, 308 kp, 309 ) 310 .ui(&txn, ui); 311 312 post_response.action.map(Into::into) 313 } 314 Route::AddColumn(route) => { 315 render_add_column_routes(ui, app, ctx, col, route); 316 317 None 318 } 319 Route::Support => { 320 SupportView::new(&mut app.support).show(ui); 321 None 322 } 323 Route::NewDeck => { 324 let id = ui.id().with("new-deck"); 325 let new_deck_state = app.view_state.id_to_deck_state.entry(id).or_default(); 326 let mut resp = None; 327 if let Some(config_resp) = ConfigureDeckView::new(new_deck_state).ui(ui) { 328 if let Some(cur_acc) = ctx.accounts.get_selected_account() { 329 app.decks_cache.add_deck( 330 cur_acc.pubkey, 331 Deck::new(config_resp.icon, config_resp.name), 332 ); 333 334 // set new deck as active 335 let cur_index = get_decks_mut(ctx.accounts, &mut app.decks_cache) 336 .decks() 337 .len() 338 - 1; 339 resp = Some(RenderNavAction::SwitchingAction(SwitchingAction::Decks( 340 DecksAction::Switch(cur_index), 341 ))); 342 } 343 344 new_deck_state.clear(); 345 get_active_columns_mut(ctx.accounts, &mut app.decks_cache) 346 .get_first_router() 347 .go_back(); 348 } 349 resp 350 } 351 Route::EditDeck(index) => { 352 let mut action = None; 353 let cur_deck = get_decks_mut(ctx.accounts, &mut app.decks_cache) 354 .decks_mut() 355 .get_mut(*index) 356 .expect("index wasn't valid"); 357 let id = ui.id().with(( 358 "edit-deck", 359 ctx.accounts.get_selected_account().map(|k| k.pubkey), 360 index, 361 )); 362 let deck_state = app 363 .view_state 364 .id_to_deck_state 365 .entry(id) 366 .or_insert_with(|| DeckState::from_deck(cur_deck)); 367 if let Some(resp) = EditDeckView::new(deck_state).ui(ui) { 368 match resp { 369 EditDeckResponse::Edit(configure_deck_response) => { 370 cur_deck.edit(configure_deck_response); 371 } 372 EditDeckResponse::Delete => { 373 action = Some(RenderNavAction::SwitchingAction(SwitchingAction::Decks( 374 DecksAction::Removing(*index), 375 ))); 376 } 377 } 378 get_active_columns_mut(ctx.accounts, &mut app.decks_cache) 379 .get_first_router() 380 .go_back(); 381 } 382 383 action 384 } 385 Route::EditProfile(pubkey) => { 386 let mut action = None; 387 if let Some(kp) = ctx.accounts.get_full(pubkey.bytes()) { 388 let state = app 389 .view_state 390 .pubkey_to_profile_state 391 .entry(*kp.pubkey) 392 .or_insert_with(|| { 393 let txn = Transaction::new(ctx.ndb).expect("txn"); 394 if let Ok(record) = ctx.ndb.get_profile_by_pubkey(&txn, kp.pubkey.bytes()) { 395 ProfileState::from_profile(&record) 396 } else { 397 ProfileState::default() 398 } 399 }); 400 if EditProfileView::new(state, ctx.img_cache).ui(ui) { 401 if let Some(taken_state) = 402 app.view_state.pubkey_to_profile_state.remove(kp.pubkey) 403 { 404 action = Some(RenderNavAction::ProfileAction(ProfileAction::SaveChanges( 405 SaveProfileChanges::new(kp.to_full(), taken_state), 406 ))) 407 } 408 } 409 } else { 410 error!("Pubkey in EditProfile route did not have an nsec attached in Accounts"); 411 } 412 action 413 } 414 } 415 } 416 417 #[must_use = "RenderNavResponse must be handled by calling .process_render_nav_response(..)"] 418 pub fn render_nav( 419 col: usize, 420 app: &mut Damus, 421 ctx: &mut AppContext<'_>, 422 ui: &mut egui::Ui, 423 ) -> RenderNavResponse { 424 let col_id = get_active_columns(ctx.accounts, &app.decks_cache).get_column_id_at_index(col); 425 // TODO(jb55): clean up this router_mut mess by using Router<R> in egui-nav directly 426 427 let nav_response = Nav::new( 428 &app.columns(ctx.accounts) 429 .column(col) 430 .router() 431 .routes() 432 .clone(), 433 ) 434 .navigating( 435 app.columns_mut(ctx.accounts) 436 .column_mut(col) 437 .router_mut() 438 .navigating, 439 ) 440 .returning( 441 app.columns_mut(ctx.accounts) 442 .column_mut(col) 443 .router_mut() 444 .returning, 445 ) 446 .id_source(egui::Id::new(col_id)) 447 .show_mut(ui, |ui, render_type, nav| match render_type { 448 NavUiType::Title => NavTitle::new( 449 ctx.ndb, 450 ctx.img_cache, 451 get_active_columns_mut(ctx.accounts, &mut app.decks_cache), 452 ctx.accounts.get_selected_account().map(|a| &a.pubkey), 453 nav.routes(), 454 col, 455 ) 456 .show(ui), 457 NavUiType::Body => render_nav_body(ui, app, ctx, nav.routes().last().expect("top"), col), 458 }); 459 460 RenderNavResponse::new(col, nav_response) 461 } 462 463 fn unsubscribe_timeline(ndb: &mut Ndb, timeline: &Timeline) { 464 if let Some(sub_id) = timeline.subscription { 465 if let Err(e) = ndb.unsubscribe(sub_id) { 466 error!("unsubscribe error: {}", e); 467 } else { 468 info!( 469 "successfully unsubscribed from timeline {} with sub id {}", 470 timeline.id, 471 sub_id.id() 472 ); 473 } 474 } 475 }