app.rs (21664B)
1 use crate::{ 2 args::ColumnsArgs, 3 column::Columns, 4 decks::{Decks, DecksCache, FALLBACK_PUBKEY}, 5 draft::Drafts, 6 nav, storage, 7 subscriptions::{SubKind, Subscriptions}, 8 support::Support, 9 timeline::{self, TimelineCache}, 10 ui::{self, DesktopSidePanel}, 11 unknowns, 12 view_state::ViewState, 13 Result, 14 }; 15 16 use notedeck::{Accounts, AppContext, DataPath, DataPathType, FilterState, ImageCache, UnknownIds}; 17 18 use enostr::{ClientMessage, Keypair, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool}; 19 use uuid::Uuid; 20 21 use egui_extras::{Size, StripBuilder}; 22 23 use nostrdb::{Ndb, Transaction}; 24 25 use std::collections::HashMap; 26 use std::path::Path; 27 use std::time::Duration; 28 use tracing::{error, info, trace, warn}; 29 30 #[derive(Debug, Eq, PartialEq, Clone)] 31 pub enum DamusState { 32 Initializing, 33 Initialized, 34 } 35 36 /// We derive Deserialize/Serialize so we can persist app state on shutdown. 37 pub struct Damus { 38 state: DamusState, 39 pub decks_cache: DecksCache, 40 pub view_state: ViewState, 41 pub drafts: Drafts, 42 pub timeline_cache: TimelineCache, 43 pub subscriptions: Subscriptions, 44 pub support: Support, 45 46 //frame_history: crate::frame_history::FrameHistory, 47 48 // TODO: make these bitflags 49 /// Were columns loaded from the commandline? If so disable persistence. 50 pub tmp_columns: bool, 51 pub debug: bool, 52 pub since_optimize: bool, 53 pub textmode: bool, 54 } 55 56 fn handle_key_events(input: &egui::InputState, columns: &mut Columns) { 57 for event in &input.raw.events { 58 if let egui::Event::Key { 59 key, pressed: true, .. 60 } = event 61 { 62 match key { 63 egui::Key::J => { 64 columns.select_down(); 65 } 66 egui::Key::K => { 67 columns.select_up(); 68 } 69 egui::Key::H => { 70 columns.select_left(); 71 } 72 egui::Key::L => { 73 columns.select_left(); 74 } 75 _ => {} 76 } 77 } 78 } 79 } 80 81 fn try_process_event( 82 damus: &mut Damus, 83 app_ctx: &mut AppContext<'_>, 84 ctx: &egui::Context, 85 ) -> Result<()> { 86 let current_columns = get_active_columns_mut(app_ctx.accounts, &mut damus.decks_cache); 87 ctx.input(|i| handle_key_events(i, current_columns)); 88 89 let ctx2 = ctx.clone(); 90 let wakeup = move || { 91 ctx2.request_repaint(); 92 }; 93 94 app_ctx.pool.keepalive_ping(wakeup); 95 96 // NOTE: we don't use the while let loop due to borrow issues 97 #[allow(clippy::while_let_loop)] 98 loop { 99 let ev = if let Some(ev) = app_ctx.pool.try_recv() { 100 ev.into_owned() 101 } else { 102 break; 103 }; 104 105 match (&ev.event).into() { 106 RelayEvent::Opened => { 107 app_ctx 108 .accounts 109 .send_initial_filters(app_ctx.pool, &ev.relay); 110 111 timeline::send_initial_timeline_filters( 112 app_ctx.ndb, 113 damus.since_optimize, 114 get_active_columns_mut(app_ctx.accounts, &mut damus.decks_cache), 115 &mut damus.subscriptions, 116 app_ctx.pool, 117 &ev.relay, 118 ); 119 } 120 // TODO: handle reconnects 121 RelayEvent::Closed => warn!("{} connection closed", &ev.relay), 122 RelayEvent::Error(e) => error!("{}: {}", &ev.relay, e), 123 RelayEvent::Other(msg) => trace!("other event {:?}", &msg), 124 RelayEvent::Message(msg) => { 125 process_message(damus, app_ctx, &ev.relay, &msg); 126 } 127 } 128 } 129 130 let current_columns = get_active_columns_mut(app_ctx.accounts, &mut damus.decks_cache); 131 let n_timelines = current_columns.timelines().len(); 132 for timeline_ind in 0..n_timelines { 133 let is_ready = { 134 let timeline = &mut current_columns.timelines[timeline_ind]; 135 timeline::is_timeline_ready( 136 app_ctx.ndb, 137 app_ctx.pool, 138 app_ctx.note_cache, 139 timeline, 140 app_ctx 141 .accounts 142 .get_selected_account() 143 .as_ref() 144 .map(|sa| &sa.pubkey), 145 ) 146 }; 147 148 if is_ready { 149 let txn = Transaction::new(app_ctx.ndb).expect("txn"); 150 // only thread timelines are reversed 151 let reversed = false; 152 153 if let Err(err) = current_columns.timelines_mut()[timeline_ind].poll_notes_into_view( 154 app_ctx.ndb, 155 &txn, 156 app_ctx.unknown_ids, 157 app_ctx.note_cache, 158 reversed, 159 ) { 160 error!("poll_notes_into_view: {err}"); 161 } 162 } else { 163 // TODO: show loading? 164 } 165 } 166 167 if app_ctx.unknown_ids.ready_to_send() { 168 unknown_id_send(app_ctx.unknown_ids, app_ctx.pool); 169 } 170 171 Ok(()) 172 } 173 174 fn unknown_id_send(unknown_ids: &mut UnknownIds, pool: &mut RelayPool) { 175 let filter = unknown_ids.filter().expect("filter"); 176 info!( 177 "Getting {} unknown ids from relays", 178 unknown_ids.ids().len() 179 ); 180 let msg = ClientMessage::req("unknownids".to_string(), filter); 181 unknown_ids.clear(); 182 pool.send(&msg); 183 } 184 185 fn update_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>, ctx: &egui::Context) { 186 match damus.state { 187 DamusState::Initializing => { 188 damus.state = DamusState::Initialized; 189 // this lets our eose handler know to close unknownids right away 190 damus 191 .subscriptions() 192 .insert("unknownids".to_string(), SubKind::OneShot); 193 if let Err(err) = timeline::setup_initial_nostrdb_subs( 194 app_ctx.ndb, 195 app_ctx.note_cache, 196 &mut damus.decks_cache, 197 ) { 198 warn!("update_damus init: {err}"); 199 } 200 } 201 202 DamusState::Initialized => (), 203 }; 204 205 if let Err(err) = try_process_event(damus, app_ctx, ctx) { 206 error!("error processing event: {}", err); 207 } 208 } 209 210 fn handle_eose( 211 damus: &mut Damus, 212 ctx: &mut AppContext<'_>, 213 subid: &str, 214 relay_url: &str, 215 ) -> Result<()> { 216 let sub_kind = if let Some(sub_kind) = damus.subscriptions().get(subid) { 217 sub_kind 218 } else { 219 let n_subids = damus.subscriptions().len(); 220 warn!( 221 "got unknown eose subid {}, {} tracked subscriptions", 222 subid, n_subids 223 ); 224 return Ok(()); 225 }; 226 227 match *sub_kind { 228 SubKind::Timeline(_) => { 229 // eose on timeline? whatevs 230 } 231 SubKind::Initial => { 232 let txn = Transaction::new(ctx.ndb)?; 233 unknowns::update_from_columns( 234 &txn, 235 ctx.unknown_ids, 236 get_active_columns(ctx.accounts, &damus.decks_cache), 237 ctx.ndb, 238 ctx.note_cache, 239 ); 240 // this is possible if this is the first time 241 if ctx.unknown_ids.ready_to_send() { 242 unknown_id_send(ctx.unknown_ids, ctx.pool); 243 } 244 } 245 246 // oneshot subs just close when they're done 247 SubKind::OneShot => { 248 let msg = ClientMessage::close(subid.to_string()); 249 ctx.pool.send_to(&msg, relay_url); 250 } 251 252 SubKind::FetchingContactList(timeline_uid) => { 253 let timeline = if let Some(tl) = 254 get_active_columns_mut(ctx.accounts, &mut damus.decks_cache) 255 .find_timeline_mut(timeline_uid) 256 { 257 tl 258 } else { 259 error!( 260 "timeline uid:{} not found for FetchingContactList", 261 timeline_uid 262 ); 263 return Ok(()); 264 }; 265 266 let filter_state = timeline.filter.get(relay_url); 267 268 // If this request was fetching a contact list, our filter 269 // state should be "FetchingRemote". We look at the local 270 // subscription for that filter state and get the subscription id 271 let local_sub = if let FilterState::FetchingRemote(unisub) = filter_state { 272 unisub.local 273 } else { 274 // TODO: we could have multiple contact list results, we need 275 // to check to see if this one is newer and use that instead 276 warn!( 277 "Expected timeline to have FetchingRemote state but was {:?}", 278 timeline.filter 279 ); 280 return Ok(()); 281 }; 282 283 info!( 284 "got contact list from {}, updating filter_state to got_remote", 285 relay_url 286 ); 287 288 // We take the subscription id and pass it to the new state of 289 // "GotRemote". This will let future frames know that it can try 290 // to look for the contact list in nostrdb. 291 timeline 292 .filter 293 .set_relay_state(relay_url.to_string(), FilterState::got_remote(local_sub)); 294 } 295 } 296 297 Ok(()) 298 } 299 300 fn process_message(damus: &mut Damus, ctx: &mut AppContext<'_>, relay: &str, msg: &RelayMessage) { 301 match msg { 302 RelayMessage::Event(_subid, ev) => { 303 let relay = if let Some(relay) = ctx.pool.relays.iter().find(|r| r.url() == relay) { 304 relay 305 } else { 306 error!("couldn't find relay {} for note processing!?", relay); 307 return; 308 }; 309 310 match relay { 311 PoolRelay::Websocket(_) => { 312 //info!("processing event {}", event); 313 if let Err(err) = ctx.ndb.process_event(ev) { 314 error!("error processing event {ev}: {err}"); 315 } 316 } 317 PoolRelay::Multicast(_) => { 318 // multicast events are client events 319 if let Err(err) = ctx.ndb.process_client_event(ev) { 320 error!("error processing multicast event {ev}: {err}"); 321 } 322 } 323 } 324 } 325 RelayMessage::Notice(msg) => warn!("Notice from {}: {}", relay, msg), 326 RelayMessage::OK(cr) => info!("OK {:?}", cr), 327 RelayMessage::Eose(sid) => { 328 if let Err(err) = handle_eose(damus, ctx, sid, relay) { 329 error!("error handling eose: {}", err); 330 } 331 } 332 } 333 } 334 335 fn render_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>, ui: &mut egui::Ui) { 336 if notedeck::ui::is_narrow(ui.ctx()) { 337 render_damus_mobile(damus, app_ctx, ui); 338 } else { 339 render_damus_desktop(damus, app_ctx, ui); 340 } 341 342 // We use this for keeping timestamps and things up to date 343 ui.ctx().request_repaint_after(Duration::from_secs(1)); 344 } 345 346 /* 347 fn determine_key_storage_type() -> KeyStorageType { 348 #[cfg(target_os = "macos")] 349 { 350 KeyStorageType::MacOS 351 } 352 353 #[cfg(target_os = "linux")] 354 { 355 KeyStorageType::Linux 356 } 357 358 #[cfg(not(any(target_os = "macos", target_os = "linux")))] 359 { 360 KeyStorageType::None 361 } 362 } 363 */ 364 365 impl Damus { 366 /// Called once before the first frame. 367 pub fn new(ctx: &mut AppContext<'_>, args: &[String]) -> Self { 368 // arg parsing 369 370 let parsed_args = ColumnsArgs::parse(args); 371 let account = ctx 372 .accounts 373 .get_selected_account() 374 .as_ref() 375 .map(|a| a.pubkey.bytes()); 376 377 let tmp_columns = !parsed_args.columns.is_empty(); 378 let decks_cache = if tmp_columns { 379 info!("DecksCache: loading from command line arguments"); 380 let mut columns: Columns = Columns::new(); 381 for col in parsed_args.columns { 382 if let Some(timeline) = col.into_timeline(ctx.ndb, account) { 383 columns.add_new_timeline_column(timeline); 384 } 385 } 386 387 columns_to_decks_cache(columns, account) 388 } else if let Some(decks_cache) = crate::storage::load_decks_cache(ctx.path, ctx.ndb) { 389 info!( 390 "DecksCache: loading from disk {}", 391 crate::storage::DECKS_CACHE_FILE 392 ); 393 decks_cache 394 } else if let Some(cols) = storage::deserialize_columns(ctx.path, ctx.ndb, account) { 395 info!( 396 "DecksCache: loading from disk at depreciated location {}", 397 crate::storage::COLUMNS_FILE 398 ); 399 columns_to_decks_cache(cols, account) 400 } else { 401 info!("DecksCache: creating new with demo configuration"); 402 let mut cache = DecksCache::new_with_demo_config(ctx.ndb); 403 for account in ctx.accounts.get_accounts() { 404 cache.add_deck_default(account.pubkey); 405 } 406 set_demo(&mut cache, ctx.ndb, ctx.accounts, ctx.unknown_ids); 407 408 cache 409 }; 410 411 let debug = ctx.args.debug; 412 let support = Support::new(ctx.path); 413 414 Self { 415 subscriptions: Subscriptions::default(), 416 since_optimize: parsed_args.since_optimize, 417 timeline_cache: TimelineCache::default(), 418 drafts: Drafts::default(), 419 state: DamusState::Initializing, 420 textmode: parsed_args.textmode, 421 //frame_history: FrameHistory::default(), 422 view_state: ViewState::default(), 423 tmp_columns, 424 support, 425 decks_cache, 426 debug, 427 } 428 } 429 430 pub fn columns_mut(&mut self, accounts: &Accounts) -> &mut Columns { 431 get_active_columns_mut(accounts, &mut self.decks_cache) 432 } 433 434 pub fn columns(&self, accounts: &Accounts) -> &Columns { 435 get_active_columns(accounts, &self.decks_cache) 436 } 437 438 pub fn gen_subid(&self, kind: &SubKind) -> String { 439 if self.debug { 440 format!("{:?}", kind) 441 } else { 442 Uuid::new_v4().to_string() 443 } 444 } 445 446 pub fn mock<P: AsRef<Path>>(data_path: P) -> Self { 447 let decks_cache = DecksCache::default(); 448 449 let path = DataPath::new(&data_path); 450 let imgcache_dir = path.path(DataPathType::Cache).join(ImageCache::rel_dir()); 451 let _ = std::fs::create_dir_all(imgcache_dir.clone()); 452 let debug = true; 453 454 let support = Support::new(&path); 455 456 Self { 457 debug, 458 subscriptions: Subscriptions::default(), 459 since_optimize: true, 460 timeline_cache: TimelineCache::default(), 461 drafts: Drafts::default(), 462 state: DamusState::Initializing, 463 textmode: false, 464 tmp_columns: true, 465 //frame_history: FrameHistory::default(), 466 view_state: ViewState::default(), 467 support, 468 decks_cache, 469 } 470 } 471 472 pub fn subscriptions(&mut self) -> &mut HashMap<String, SubKind> { 473 &mut self.subscriptions.subs 474 } 475 } 476 477 /* 478 fn circle_icon(ui: &mut egui::Ui, openness: f32, response: &egui::Response) { 479 let stroke = ui.style().interact(&response).fg_stroke; 480 let radius = egui::lerp(2.0..=3.0, openness); 481 ui.painter() 482 .circle_filled(response.rect.center(), radius, stroke.color); 483 } 484 */ 485 486 fn render_damus_mobile(app: &mut Damus, app_ctx: &mut AppContext<'_>, ui: &mut egui::Ui) { 487 #[cfg(feature = "profiling")] 488 puffin::profile_function!(); 489 490 //let routes = app.timelines[0].routes.clone(); 491 492 if !app.columns(app_ctx.accounts).columns().is_empty() 493 && nav::render_nav(0, app, app_ctx, ui).process_render_nav_response(app, app_ctx) 494 && !app.tmp_columns 495 { 496 storage::save_decks_cache(app_ctx.path, &app.decks_cache); 497 } 498 } 499 500 fn render_damus_desktop(app: &mut Damus, app_ctx: &mut AppContext<'_>, ui: &mut egui::Ui) { 501 #[cfg(feature = "profiling")] 502 puffin::profile_function!(); 503 504 let screen_size = ui.ctx().screen_rect().width(); 505 let calc_panel_width = (screen_size 506 / get_active_columns(app_ctx.accounts, &app.decks_cache).num_columns() as f32) 507 - 30.0; 508 let min_width = 320.0; 509 let need_scroll = calc_panel_width < min_width; 510 let panel_sizes = if need_scroll { 511 Size::exact(min_width) 512 } else { 513 Size::remainder() 514 }; 515 516 ui.spacing_mut().item_spacing.x = 0.0; 517 if need_scroll { 518 egui::ScrollArea::horizontal().show(ui, |ui| { 519 timelines_view(ui, panel_sizes, app, app_ctx); 520 }); 521 } else { 522 timelines_view(ui, panel_sizes, app, app_ctx); 523 } 524 } 525 526 fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, ctx: &mut AppContext<'_>) { 527 StripBuilder::new(ui) 528 .size(Size::exact(ui::side_panel::SIDE_PANEL_WIDTH)) 529 .sizes( 530 sizes, 531 get_active_columns(ctx.accounts, &app.decks_cache).num_columns(), 532 ) 533 .clip(true) 534 .horizontal(|mut strip| { 535 let mut side_panel_action: Option<nav::SwitchingAction> = None; 536 strip.cell(|ui| { 537 let rect = ui.available_rect_before_wrap(); 538 let side_panel = DesktopSidePanel::new( 539 ctx.ndb, 540 ctx.img_cache, 541 ctx.accounts.get_selected_account(), 542 &app.decks_cache, 543 ) 544 .show(ui); 545 546 if side_panel.response.clicked() || side_panel.response.secondary_clicked() { 547 if let Some(action) = DesktopSidePanel::perform_action( 548 &mut app.decks_cache, 549 ctx.accounts, 550 &mut app.support, 551 ctx.theme, 552 side_panel.action, 553 ) { 554 side_panel_action = Some(action); 555 } 556 } 557 558 // vertical sidebar line 559 ui.painter().vline( 560 rect.right(), 561 rect.y_range(), 562 ui.visuals().widgets.noninteractive.bg_stroke, 563 ); 564 }); 565 566 let mut save_cols = false; 567 if let Some(action) = side_panel_action { 568 save_cols = save_cols || action.process(&mut app.decks_cache, ctx); 569 } 570 571 let num_cols = app.columns(ctx.accounts).num_columns(); 572 let mut responses = Vec::with_capacity(num_cols); 573 for col_index in 0..num_cols { 574 strip.cell(|ui| { 575 let rect = ui.available_rect_before_wrap(); 576 responses.push(nav::render_nav(col_index, app, ctx, ui)); 577 578 // vertical line 579 ui.painter().vline( 580 rect.right(), 581 rect.y_range(), 582 ui.visuals().widgets.noninteractive.bg_stroke, 583 ); 584 }); 585 586 //strip.cell(|ui| timeline::timeline_view(ui, app, timeline_ind)); 587 } 588 589 for response in responses { 590 let save = response.process_render_nav_response(app, ctx); 591 save_cols = save_cols || save; 592 } 593 594 if app.tmp_columns { 595 save_cols = false; 596 } 597 598 if save_cols { 599 storage::save_decks_cache(ctx.path, &app.decks_cache); 600 } 601 }); 602 } 603 604 impl notedeck::App for Damus { 605 fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) { 606 /* 607 self.app 608 .frame_history 609 .on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage); 610 */ 611 612 update_damus(self, ctx, ui.ctx()); 613 render_damus(self, ctx, ui); 614 } 615 } 616 617 pub fn get_active_columns<'a>(accounts: &Accounts, decks_cache: &'a DecksCache) -> &'a Columns { 618 get_decks(accounts, decks_cache).active().columns() 619 } 620 621 pub fn get_decks<'a>(accounts: &Accounts, decks_cache: &'a DecksCache) -> &'a Decks { 622 let key = if let Some(acc) = accounts.get_selected_account() { 623 &acc.pubkey 624 } else { 625 decks_cache.get_fallback_pubkey() 626 }; 627 decks_cache.decks(key) 628 } 629 630 pub fn get_active_columns_mut<'a>( 631 accounts: &Accounts, 632 decks_cache: &'a mut DecksCache, 633 ) -> &'a mut Columns { 634 get_decks_mut(accounts, decks_cache) 635 .active_mut() 636 .columns_mut() 637 } 638 639 pub fn get_decks_mut<'a>(accounts: &Accounts, decks_cache: &'a mut DecksCache) -> &'a mut Decks { 640 if let Some(acc) = accounts.get_selected_account() { 641 decks_cache.decks_mut(&acc.pubkey) 642 } else { 643 decks_cache.fallback_mut() 644 } 645 } 646 647 pub fn set_demo( 648 decks_cache: &mut DecksCache, 649 ndb: &Ndb, 650 accounts: &mut Accounts, 651 unk_ids: &mut UnknownIds, 652 ) { 653 let txn = Transaction::new(ndb).expect("txn"); 654 accounts 655 .add_account(Keypair::only_pubkey(*decks_cache.get_fallback_pubkey())) 656 .process_action(unk_ids, ndb, &txn); 657 accounts.select_account(accounts.num_accounts() - 1); 658 } 659 660 fn columns_to_decks_cache(cols: Columns, key: Option<&[u8; 32]>) -> DecksCache { 661 let mut account_to_decks: HashMap<Pubkey, Decks> = Default::default(); 662 let decks = Decks::new(crate::decks::Deck::new_with_columns( 663 crate::decks::Deck::default().icon, 664 "My Deck".to_owned(), 665 cols, 666 )); 667 668 let account = if let Some(key) = key { 669 Pubkey::new(*key) 670 } else { 671 FALLBACK_PUBKEY() 672 }; 673 account_to_decks.insert(account, decks); 674 DecksCache::new(account_to_decks) 675 }