app.rs (25286B)
1 use crate::{ 2 accounts::{Accounts, AccountsRoute}, 3 app_creation::setup_cc, 4 app_size_handler::AppSizeHandler, 5 args::Args, 6 column::{Column, Columns}, 7 draft::Drafts, 8 filter::FilterState, 9 frame_history::FrameHistory, 10 imgcache::ImageCache, 11 nav, 12 notecache::NoteCache, 13 notes_holder::NotesHolderStorage, 14 profile::Profile, 15 route::Route, 16 storage::{self, DataPath, DataPathType, Directory, FileKeyStorage, KeyStorageType}, 17 subscriptions::{SubKind, Subscriptions}, 18 support::Support, 19 thread::Thread, 20 timeline::{self, Timeline, TimelineKind}, 21 ui::{self, add_column::AddColumnRoute, DesktopSidePanel}, 22 unknowns::UnknownIds, 23 view_state::ViewState, 24 Result, 25 }; 26 27 use enostr::{ClientMessage, Keypair, Pubkey, RelayEvent, RelayMessage, RelayPool}; 28 use uuid::Uuid; 29 30 use egui::{Context, Frame, Style}; 31 use egui_extras::{Size, StripBuilder}; 32 33 use nostrdb::{Config, Filter, Ndb, Transaction}; 34 35 use std::collections::HashMap; 36 use std::path::Path; 37 use std::time::Duration; 38 use tracing::{error, info, trace, warn}; 39 40 #[derive(Debug, Eq, PartialEq, Clone)] 41 pub enum DamusState { 42 Initializing, 43 Initialized, 44 } 45 46 /// We derive Deserialize/Serialize so we can persist app state on shutdown. 47 pub struct Damus { 48 state: DamusState, 49 pub note_cache: NoteCache, 50 pub pool: RelayPool, 51 52 pub columns: Columns, 53 pub ndb: Ndb, 54 pub view_state: ViewState, 55 pub unknown_ids: UnknownIds, 56 pub drafts: Drafts, 57 pub threads: NotesHolderStorage<Thread>, 58 pub profiles: NotesHolderStorage<Profile>, 59 pub img_cache: ImageCache, 60 pub accounts: Accounts, 61 pub subscriptions: Subscriptions, 62 pub app_rect_handler: AppSizeHandler, 63 pub support: Support, 64 65 frame_history: crate::frame_history::FrameHistory, 66 67 pub path: DataPath, 68 // TODO: make these bitflags 69 pub debug: bool, 70 pub since_optimize: bool, 71 pub textmode: bool, 72 } 73 74 fn handle_key_events(input: &egui::InputState, _pixels_per_point: f32, columns: &mut Columns) { 75 for event in &input.raw.events { 76 if let egui::Event::Key { 77 key, pressed: true, .. 78 } = event 79 { 80 match key { 81 egui::Key::J => { 82 columns.select_down(); 83 } 84 egui::Key::K => { 85 columns.select_up(); 86 } 87 egui::Key::H => { 88 columns.select_left(); 89 } 90 egui::Key::L => { 91 columns.select_left(); 92 } 93 _ => {} 94 } 95 } 96 } 97 } 98 99 fn try_process_event(damus: &mut Damus, ctx: &egui::Context) -> Result<()> { 100 let ppp = ctx.pixels_per_point(); 101 ctx.input(|i| handle_key_events(i, ppp, &mut damus.columns)); 102 103 let ctx2 = ctx.clone(); 104 let wakeup = move || { 105 ctx2.request_repaint(); 106 }; 107 damus.pool.keepalive_ping(wakeup); 108 109 // NOTE: we don't use the while let loop due to borrow issues 110 #[allow(clippy::while_let_loop)] 111 loop { 112 let ev = if let Some(ev) = damus.pool.try_recv() { 113 ev.into_owned() 114 } else { 115 break; 116 }; 117 118 match (&ev.event).into() { 119 RelayEvent::Opened => { 120 damus 121 .accounts 122 .send_initial_filters(&mut damus.pool, &ev.relay); 123 124 timeline::send_initial_timeline_filters( 125 &damus.ndb, 126 damus.since_optimize, 127 &mut damus.columns, 128 &mut damus.subscriptions, 129 &mut damus.pool, 130 &ev.relay, 131 ); 132 } 133 // TODO: handle reconnects 134 RelayEvent::Closed => warn!("{} connection closed", &ev.relay), 135 RelayEvent::Error(e) => error!("{}: {}", &ev.relay, e), 136 RelayEvent::Other(msg) => trace!("other event {:?}", &msg), 137 RelayEvent::Message(msg) => process_message(damus, &ev.relay, &msg), 138 } 139 } 140 141 let n_timelines = damus.columns.timelines().len(); 142 for timeline_ind in 0..n_timelines { 143 let is_ready = { 144 let timeline = &mut damus.columns.timelines[timeline_ind]; 145 timeline::is_timeline_ready( 146 &damus.ndb, 147 &mut damus.pool, 148 &mut damus.note_cache, 149 timeline, 150 &damus.accounts.mutefun(), 151 ) 152 }; 153 154 if is_ready { 155 let txn = Transaction::new(&damus.ndb).expect("txn"); 156 157 if let Err(err) = Timeline::poll_notes_into_view( 158 timeline_ind, 159 damus.columns.timelines_mut(), 160 &damus.ndb, 161 &txn, 162 &mut damus.unknown_ids, 163 &mut damus.note_cache, 164 &damus.accounts.mutefun(), 165 ) { 166 error!("poll_notes_into_view: {err}"); 167 } 168 } else { 169 // TODO: show loading? 170 } 171 } 172 173 if damus.unknown_ids.ready_to_send() { 174 unknown_id_send(damus); 175 } 176 177 Ok(()) 178 } 179 180 fn unknown_id_send(damus: &mut Damus) { 181 let filter = damus.unknown_ids.filter().expect("filter"); 182 info!( 183 "Getting {} unknown ids from relays", 184 damus.unknown_ids.ids().len() 185 ); 186 let msg = ClientMessage::req("unknownids".to_string(), filter); 187 damus.unknown_ids.clear(); 188 damus.pool.send(&msg); 189 } 190 191 #[cfg(feature = "profiling")] 192 fn setup_profiling() { 193 puffin::set_scopes_on(true); // tell puffin to collect data 194 } 195 196 fn update_damus(damus: &mut Damus, ctx: &egui::Context) { 197 damus.accounts.update(&damus.ndb, &mut damus.pool, ctx); // update user relay and mute lists 198 199 match damus.state { 200 DamusState::Initializing => { 201 #[cfg(feature = "profiling")] 202 setup_profiling(); 203 204 damus.state = DamusState::Initialized; 205 // this lets our eose handler know to close unknownids right away 206 damus 207 .subscriptions() 208 .insert("unknownids".to_string(), SubKind::OneShot); 209 if let Err(err) = timeline::setup_initial_nostrdb_subs( 210 &damus.ndb, 211 &mut damus.note_cache, 212 &mut damus.columns, 213 &damus.accounts.mutefun(), 214 ) { 215 warn!("update_damus init: {err}"); 216 } 217 } 218 219 DamusState::Initialized => (), 220 }; 221 222 if let Err(err) = try_process_event(damus, ctx) { 223 error!("error processing event: {}", err); 224 } 225 226 damus.app_rect_handler.try_save_app_size(ctx); 227 } 228 229 fn process_event(damus: &mut Damus, _subid: &str, event: &str) { 230 #[cfg(feature = "profiling")] 231 puffin::profile_function!(); 232 233 //info!("processing event {}", event); 234 if let Err(_err) = damus.ndb.process_event(event) { 235 error!("error processing event {}", event); 236 } 237 } 238 239 fn handle_eose(damus: &mut Damus, subid: &str, relay_url: &str) -> Result<()> { 240 let sub_kind = if let Some(sub_kind) = damus.subscriptions().get(subid) { 241 sub_kind 242 } else { 243 let n_subids = damus.subscriptions().len(); 244 warn!( 245 "got unknown eose subid {}, {} tracked subscriptions", 246 subid, n_subids 247 ); 248 return Ok(()); 249 }; 250 251 match *sub_kind { 252 SubKind::Timeline(_) => { 253 // eose on timeline? whatevs 254 } 255 SubKind::Initial => { 256 let txn = Transaction::new(&damus.ndb)?; 257 UnknownIds::update( 258 &txn, 259 &mut damus.unknown_ids, 260 &damus.columns, 261 &damus.ndb, 262 &mut damus.note_cache, 263 ); 264 // this is possible if this is the first time 265 if damus.unknown_ids.ready_to_send() { 266 unknown_id_send(damus); 267 } 268 } 269 270 // oneshot subs just close when they're done 271 SubKind::OneShot => { 272 let msg = ClientMessage::close(subid.to_string()); 273 damus.pool.send_to(&msg, relay_url); 274 } 275 276 SubKind::FetchingContactList(timeline_uid) => { 277 let timeline = if let Some(tl) = damus.columns.find_timeline_mut(timeline_uid) { 278 tl 279 } else { 280 error!( 281 "timeline uid:{} not found for FetchingContactList", 282 timeline_uid 283 ); 284 return Ok(()); 285 }; 286 287 let filter_state = timeline.filter.get(relay_url); 288 289 // If this request was fetching a contact list, our filter 290 // state should be "FetchingRemote". We look at the local 291 // subscription for that filter state and get the subscription id 292 let local_sub = if let FilterState::FetchingRemote(unisub) = filter_state { 293 unisub.local 294 } else { 295 // TODO: we could have multiple contact list results, we need 296 // to check to see if this one is newer and use that instead 297 warn!( 298 "Expected timeline to have FetchingRemote state but was {:?}", 299 timeline.filter 300 ); 301 return Ok(()); 302 }; 303 304 info!( 305 "got contact list from {}, updating filter_state to got_remote", 306 relay_url 307 ); 308 309 // We take the subscription id and pass it to the new state of 310 // "GotRemote". This will let future frames know that it can try 311 // to look for the contact list in nostrdb. 312 timeline 313 .filter 314 .set_relay_state(relay_url.to_string(), FilterState::got_remote(local_sub)); 315 } 316 } 317 318 Ok(()) 319 } 320 321 fn process_message(damus: &mut Damus, relay: &str, msg: &RelayMessage) { 322 match msg { 323 RelayMessage::Event(subid, ev) => process_event(damus, subid, ev), 324 RelayMessage::Notice(msg) => warn!("Notice from {}: {}", relay, msg), 325 RelayMessage::OK(cr) => info!("OK {:?}", cr), 326 RelayMessage::Eose(sid) => { 327 if let Err(err) = handle_eose(damus, sid, relay) { 328 error!("error handling eose: {}", err); 329 } 330 } 331 } 332 } 333 334 fn render_damus(damus: &mut Damus, ctx: &Context) { 335 if ui::is_narrow(ctx) { 336 render_damus_mobile(ctx, damus); 337 } else { 338 render_damus_desktop(ctx, damus); 339 } 340 341 ctx.request_repaint_after(Duration::from_secs(1)); 342 343 #[cfg(feature = "profiling")] 344 puffin_egui::profiler_window(ctx); 345 } 346 347 /* 348 fn determine_key_storage_type() -> KeyStorageType { 349 #[cfg(target_os = "macos")] 350 { 351 KeyStorageType::MacOS 352 } 353 354 #[cfg(target_os = "linux")] 355 { 356 KeyStorageType::Linux 357 } 358 359 #[cfg(not(any(target_os = "macos", target_os = "linux")))] 360 { 361 KeyStorageType::None 362 } 363 } 364 */ 365 366 impl Damus { 367 /// Called once before the first frame. 368 pub fn new<P: AsRef<Path>>(ctx: &egui::Context, data_path: P, args: Vec<String>) -> Self { 369 // arg parsing 370 let parsed_args = Args::parse(&args); 371 let is_mobile = parsed_args.is_mobile.unwrap_or(ui::is_compiled_as_mobile()); 372 373 // Some people have been running notedeck in debug, let's catch that! 374 if !cfg!(test) && cfg!(debug_assertions) && !parsed_args.debug { 375 println!("--- WELCOME TO DAMUS NOTEDECK! ---"); 376 println!("It looks like are running notedeck in debug mode, unless you are a developer, this is not likely what you want."); 377 println!("If you are a developer, run `cargo run -- --debug` to skip this message."); 378 println!("For everyone else, try again with `cargo run --release`. Enjoy!"); 379 println!("---------------------------------"); 380 panic!(); 381 } 382 383 setup_cc(ctx, is_mobile, parsed_args.light); 384 385 let data_path = parsed_args 386 .datapath 387 .unwrap_or(data_path.as_ref().to_str().expect("db path ok").to_string()); 388 let path = DataPath::new(&data_path); 389 let dbpath_str = parsed_args 390 .dbpath 391 .unwrap_or_else(|| path.path(DataPathType::Db).to_str().unwrap().to_string()); 392 393 let _ = std::fs::create_dir_all(&dbpath_str); 394 395 let imgcache_dir = path.path(DataPathType::Cache).join(ImageCache::rel_dir()); 396 let _ = std::fs::create_dir_all(imgcache_dir.clone()); 397 398 let mapsize = if cfg!(target_os = "windows") { 399 // 16 Gib on windows because it actually creates the file 400 1024usize * 1024usize * 1024usize * 16usize 401 } else { 402 // 1 TiB for everything else since its just virtually mapped 403 1024usize * 1024usize * 1024usize * 1024usize 404 }; 405 406 let config = Config::new().set_ingester_threads(4).set_mapsize(mapsize); 407 408 let keystore = if parsed_args.use_keystore { 409 let keys_path = path.path(DataPathType::Keys); 410 let selected_key_path = path.path(DataPathType::SelectedKey); 411 KeyStorageType::FileSystem(FileKeyStorage::new( 412 Directory::new(keys_path), 413 Directory::new(selected_key_path), 414 )) 415 } else { 416 KeyStorageType::None 417 }; 418 419 let mut accounts = Accounts::new(keystore, parsed_args.relays); 420 421 let num_keys = parsed_args.keys.len(); 422 423 let mut unknown_ids = UnknownIds::default(); 424 let ndb = Ndb::new(&dbpath_str, &config).expect("ndb"); 425 426 { 427 let txn = Transaction::new(&ndb).expect("txn"); 428 for key in parsed_args.keys { 429 info!("adding account: {}", key.pubkey); 430 accounts 431 .add_account(key) 432 .process_action(&mut unknown_ids, &ndb, &txn); 433 } 434 } 435 436 if num_keys != 0 { 437 accounts.select_account(0); 438 } 439 440 // AccountManager will setup the pool on first update 441 let pool = RelayPool::new(); 442 443 let account = accounts 444 .get_selected_account() 445 .as_ref() 446 .map(|a| a.pubkey.bytes()); 447 448 let mut columns = if parsed_args.columns.is_empty() { 449 if let Some(serializable_columns) = storage::load_columns(&path) { 450 info!("Using columns from disk"); 451 serializable_columns.into_columns(&ndb, account) 452 } else { 453 info!("Could not load columns from disk"); 454 Columns::new() 455 } 456 } else { 457 info!( 458 "Using columns from command line arguments: {:?}", 459 parsed_args.columns 460 ); 461 let mut columns: Columns = Columns::new(); 462 for col in parsed_args.columns { 463 if let Some(timeline) = col.into_timeline(&ndb, account) { 464 columns.add_new_timeline_column(timeline); 465 } 466 } 467 468 columns 469 }; 470 471 let debug = parsed_args.debug; 472 473 if columns.columns().is_empty() { 474 if accounts.get_accounts().is_empty() { 475 set_demo(&path, &ndb, &mut accounts, &mut columns, &mut unknown_ids); 476 } else { 477 columns.new_column_picker(); 478 } 479 } 480 481 let app_rect_handler = AppSizeHandler::new(&path); 482 let support = Support::new(&path); 483 484 Self { 485 pool, 486 debug, 487 unknown_ids, 488 subscriptions: Subscriptions::default(), 489 since_optimize: parsed_args.since_optimize, 490 threads: NotesHolderStorage::default(), 491 profiles: NotesHolderStorage::default(), 492 drafts: Drafts::default(), 493 state: DamusState::Initializing, 494 img_cache: ImageCache::new(imgcache_dir), 495 note_cache: NoteCache::default(), 496 columns, 497 textmode: parsed_args.textmode, 498 ndb, 499 accounts, 500 frame_history: FrameHistory::default(), 501 view_state: ViewState::default(), 502 path, 503 app_rect_handler, 504 support, 505 } 506 } 507 508 pub fn pool_mut(&mut self) -> &mut RelayPool { 509 &mut self.pool 510 } 511 512 pub fn ndb(&self) -> &Ndb { 513 &self.ndb 514 } 515 516 pub fn drafts_mut(&mut self) -> &mut Drafts { 517 &mut self.drafts 518 } 519 520 pub fn img_cache_mut(&mut self) -> &mut ImageCache { 521 &mut self.img_cache 522 } 523 524 pub fn accounts(&self) -> &Accounts { 525 &self.accounts 526 } 527 528 pub fn accounts_mut(&mut self) -> &mut Accounts { 529 &mut self.accounts 530 } 531 532 pub fn view_state_mut(&mut self) -> &mut ViewState { 533 &mut self.view_state 534 } 535 536 pub fn columns_mut(&mut self) -> &mut Columns { 537 &mut self.columns 538 } 539 540 pub fn columns(&self) -> &Columns { 541 &self.columns 542 } 543 544 pub fn gen_subid(&self, kind: &SubKind) -> String { 545 if self.debug { 546 format!("{:?}", kind) 547 } else { 548 Uuid::new_v4().to_string() 549 } 550 } 551 552 pub fn mock<P: AsRef<Path>>(data_path: P) -> Self { 553 let mut columns = Columns::new(); 554 let filter = Filter::from_json(include_str!("../queries/global.json")).unwrap(); 555 556 let timeline = Timeline::new(TimelineKind::Universe, FilterState::ready(vec![filter])); 557 558 columns.add_new_timeline_column(timeline); 559 560 let path = DataPath::new(&data_path); 561 let imgcache_dir = path.path(DataPathType::Cache).join(ImageCache::rel_dir()); 562 let _ = std::fs::create_dir_all(imgcache_dir.clone()); 563 let debug = true; 564 565 let app_rect_handler = AppSizeHandler::new(&path); 566 let support = Support::new(&path); 567 568 let config = Config::new().set_ingester_threads(2); 569 570 Self { 571 debug, 572 unknown_ids: UnknownIds::default(), 573 subscriptions: Subscriptions::default(), 574 since_optimize: true, 575 threads: NotesHolderStorage::default(), 576 profiles: NotesHolderStorage::default(), 577 drafts: Drafts::default(), 578 state: DamusState::Initializing, 579 pool: RelayPool::new(), 580 img_cache: ImageCache::new(imgcache_dir), 581 note_cache: NoteCache::default(), 582 columns, 583 textmode: false, 584 ndb: Ndb::new( 585 path.path(DataPathType::Db) 586 .to_str() 587 .expect("db path should be ok"), 588 &config, 589 ) 590 .expect("ndb"), 591 accounts: Accounts::new(KeyStorageType::None, vec![]), 592 frame_history: FrameHistory::default(), 593 view_state: ViewState::default(), 594 595 path, 596 app_rect_handler, 597 support, 598 } 599 } 600 601 pub fn subscriptions(&mut self) -> &mut HashMap<String, SubKind> { 602 &mut self.subscriptions.subs 603 } 604 605 pub fn note_cache_mut(&mut self) -> &mut NoteCache { 606 &mut self.note_cache 607 } 608 609 pub fn unknown_ids_mut(&mut self) -> &mut UnknownIds { 610 &mut self.unknown_ids 611 } 612 613 pub fn threads(&self) -> &NotesHolderStorage<Thread> { 614 &self.threads 615 } 616 617 pub fn threads_mut(&mut self) -> &mut NotesHolderStorage<Thread> { 618 &mut self.threads 619 } 620 621 pub fn note_cache(&self) -> &NoteCache { 622 &self.note_cache 623 } 624 } 625 626 /* 627 fn circle_icon(ui: &mut egui::Ui, openness: f32, response: &egui::Response) { 628 let stroke = ui.style().interact(&response).fg_stroke; 629 let radius = egui::lerp(2.0..=3.0, openness); 630 ui.painter() 631 .circle_filled(response.rect.center(), radius, stroke.color); 632 } 633 */ 634 635 fn render_damus_mobile(ctx: &egui::Context, app: &mut Damus) { 636 #[cfg(feature = "profiling")] 637 puffin::profile_function!(); 638 639 //let routes = app.timelines[0].routes.clone(); 640 641 main_panel(&ctx.style(), ui::is_narrow(ctx)).show(ctx, |ui| { 642 if !app.columns.columns().is_empty() 643 && nav::render_nav(0, app, ui).process_render_nav_response(app) 644 { 645 storage::save_columns(&app.path, app.columns().as_serializable_columns()); 646 } 647 }); 648 } 649 650 fn main_panel(style: &Style, narrow: bool) -> egui::CentralPanel { 651 let inner_margin = egui::Margin { 652 top: if narrow { 50.0 } else { 0.0 }, 653 left: 0.0, 654 right: 0.0, 655 bottom: 0.0, 656 }; 657 egui::CentralPanel::default().frame(Frame { 658 inner_margin, 659 fill: style.visuals.panel_fill, 660 ..Default::default() 661 }) 662 } 663 664 fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus) { 665 #[cfg(feature = "profiling")] 666 puffin::profile_function!(); 667 668 let screen_size = ctx.screen_rect().width(); 669 let calc_panel_width = (screen_size / app.columns.num_columns() as f32) - 30.0; 670 let min_width = 320.0; 671 let need_scroll = calc_panel_width < min_width; 672 let panel_sizes = if need_scroll { 673 Size::exact(min_width) 674 } else { 675 Size::remainder() 676 }; 677 678 main_panel(&ctx.style(), ui::is_narrow(ctx)).show(ctx, |ui| { 679 ui.spacing_mut().item_spacing.x = 0.0; 680 if need_scroll { 681 egui::ScrollArea::horizontal().show(ui, |ui| { 682 timelines_view(ui, panel_sizes, app); 683 }); 684 } else { 685 timelines_view(ui, panel_sizes, app); 686 } 687 }); 688 } 689 690 fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus) { 691 StripBuilder::new(ui) 692 .size(Size::exact(ui::side_panel::SIDE_PANEL_WIDTH)) 693 .sizes(sizes, app.columns.num_columns()) 694 .clip(true) 695 .horizontal(|mut strip| { 696 strip.cell(|ui| { 697 let rect = ui.available_rect_before_wrap(); 698 let side_panel = DesktopSidePanel::new( 699 &app.ndb, 700 &mut app.img_cache, 701 app.accounts.get_selected_account(), 702 ) 703 .show(ui); 704 705 if side_panel.response.clicked() { 706 DesktopSidePanel::perform_action( 707 &mut app.columns, 708 &mut app.support, 709 side_panel.action, 710 ); 711 } 712 713 // vertical sidebar line 714 ui.painter().vline( 715 rect.right(), 716 rect.y_range(), 717 ui.visuals().widgets.noninteractive.bg_stroke, 718 ); 719 }); 720 721 let mut responses = Vec::with_capacity(app.columns.num_columns()); 722 for col_index in 0..app.columns.num_columns() { 723 strip.cell(|ui| { 724 let rect = ui.available_rect_before_wrap(); 725 responses.push(nav::render_nav(col_index, app, ui)); 726 727 // vertical line 728 ui.painter().vline( 729 rect.right(), 730 rect.y_range(), 731 ui.visuals().widgets.noninteractive.bg_stroke, 732 ); 733 }); 734 735 //strip.cell(|ui| timeline::timeline_view(ui, app, timeline_ind)); 736 } 737 738 let mut save_cols = false; 739 for response in responses { 740 let save = response.process_render_nav_response(app); 741 save_cols = save_cols || save; 742 } 743 744 if save_cols { 745 storage::save_columns(&app.path, app.columns().as_serializable_columns()); 746 } 747 }); 748 } 749 750 impl eframe::App for Damus { 751 /// Called by the frame work to save state before shutdown. 752 fn save(&mut self, _storage: &mut dyn eframe::Storage) { 753 //eframe::set_value(storage, eframe::APP_KEY, self); 754 } 755 756 /// Called each time the UI needs repainting, which may be many times per second. 757 /// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`. 758 fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { 759 self.frame_history 760 .on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage); 761 762 #[cfg(feature = "profiling")] 763 puffin::GlobalProfiler::lock().new_frame(); 764 update_damus(self, ctx); 765 render_damus(self, ctx); 766 } 767 } 768 769 fn set_demo( 770 data_path: &DataPath, 771 ndb: &Ndb, 772 accounts: &mut Accounts, 773 columns: &mut Columns, 774 unk_ids: &mut UnknownIds, 775 ) { 776 let demo_pubkey = 777 Pubkey::from_hex("aa733081e4f0f79dd43023d8983265593f2b41a988671cfcef3f489b91ad93fe") 778 .unwrap(); 779 { 780 let txn = Transaction::new(ndb).expect("txn"); 781 accounts 782 .add_account(Keypair::only_pubkey(demo_pubkey)) 783 .process_action(unk_ids, ndb, &txn); 784 accounts.select_account(0); 785 } 786 787 columns.add_column(Column::new(vec![ 788 Route::AddColumn(AddColumnRoute::Base), 789 Route::Accounts(AccountsRoute::Accounts), 790 ])); 791 792 if let Some(timeline) = 793 TimelineKind::contact_list(timeline::PubkeySource::Explicit(demo_pubkey)) 794 .into_timeline(ndb, Some(demo_pubkey.bytes())) 795 { 796 columns.add_new_timeline_column(timeline); 797 } 798 799 columns.add_new_timeline_column(Timeline::hashtag("introductions".to_string())); 800 801 storage::save_columns(data_path, columns.as_serializable_columns()); 802 }