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