mod.rs (23894B)
1 use crate::{ 2 error::Error, 3 multi_subscriber::MultiSubscriber, 4 subscriptions::{self, SubKind, Subscriptions}, 5 timeline::kind::ListKind, 6 Result, 7 }; 8 9 use notedeck::{ 10 filter, CachedNote, FilterError, FilterState, FilterStates, NoteCache, NoteRef, UnknownIds, 11 }; 12 13 use egui_virtual_list::VirtualList; 14 use enostr::{PoolRelay, Pubkey, RelayPool}; 15 use nostrdb::{Filter, Ndb, Note, NoteKey, Transaction}; 16 use std::cell::RefCell; 17 use std::rc::Rc; 18 19 use tracing::{debug, error, info, warn}; 20 21 pub mod cache; 22 pub mod kind; 23 pub mod route; 24 25 pub use cache::TimelineCache; 26 pub use kind::{ColumnTitle, PubkeySource, ThreadSelection, TimelineKind}; 27 28 //#[derive(Debug, Hash, Clone, Eq, PartialEq)] 29 //pub type TimelineId = TimelineKind; 30 31 /* 32 33 impl TimelineId { 34 pub fn kind(&self) -> &TimelineKind { 35 &self.kind 36 } 37 38 pub fn new(id: TimelineKind) -> Self { 39 TimelineId(id) 40 } 41 42 pub fn profile(pubkey: Pubkey) -> Self { 43 TimelineId::new(TimelineKind::Profile(PubkeySource::pubkey(pubkey))) 44 } 45 } 46 47 impl fmt::Display for TimelineId { 48 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 49 write!(f, "TimelineId({})", self.0) 50 } 51 } 52 */ 53 54 #[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] 55 pub enum ViewFilter { 56 Notes, 57 58 #[default] 59 NotesAndReplies, 60 } 61 62 impl ViewFilter { 63 pub fn name(&self) -> &'static str { 64 match self { 65 ViewFilter::Notes => "Notes", 66 ViewFilter::NotesAndReplies => "Notes & Replies", 67 } 68 } 69 70 pub fn filter_notes(cache: &CachedNote, note: &Note) -> bool { 71 !cache.reply.borrow(note.tags()).is_reply() 72 } 73 74 fn identity(_cache: &CachedNote, _note: &Note) -> bool { 75 true 76 } 77 78 pub fn filter(&self) -> fn(&CachedNote, &Note) -> bool { 79 match self { 80 ViewFilter::Notes => ViewFilter::filter_notes, 81 ViewFilter::NotesAndReplies => ViewFilter::identity, 82 } 83 } 84 } 85 86 /// A timeline view is a filtered view of notes in a timeline. Two standard views 87 /// are "Notes" and "Notes & Replies". A timeline is associated with a Filter, 88 /// but a TimelineTab is a further filtered view of this Filter that can't 89 /// be captured by a Filter itself. 90 #[derive(Default, Debug)] 91 pub struct TimelineTab { 92 pub notes: Vec<NoteRef>, 93 pub selection: i32, 94 pub filter: ViewFilter, 95 pub list: Rc<RefCell<VirtualList>>, 96 } 97 98 impl TimelineTab { 99 pub fn new(filter: ViewFilter) -> Self { 100 TimelineTab::new_with_capacity(filter, 1000) 101 } 102 103 pub fn only_notes_and_replies() -> Vec<Self> { 104 vec![TimelineTab::new(ViewFilter::NotesAndReplies)] 105 } 106 107 pub fn no_replies() -> Vec<Self> { 108 vec![TimelineTab::new(ViewFilter::Notes)] 109 } 110 111 pub fn full_tabs() -> Vec<Self> { 112 vec![ 113 TimelineTab::new(ViewFilter::Notes), 114 TimelineTab::new(ViewFilter::NotesAndReplies), 115 ] 116 } 117 118 pub fn new_with_capacity(filter: ViewFilter, cap: usize) -> Self { 119 let selection = 0i32; 120 let mut list = VirtualList::new(); 121 list.hide_on_resize(None); 122 list.over_scan(1000.0); 123 let list = Rc::new(RefCell::new(list)); 124 let notes: Vec<NoteRef> = Vec::with_capacity(cap); 125 126 TimelineTab { 127 notes, 128 selection, 129 filter, 130 list, 131 } 132 } 133 134 fn insert(&mut self, new_refs: &[NoteRef], reversed: bool) { 135 if new_refs.is_empty() { 136 return; 137 } 138 let num_prev_items = self.notes.len(); 139 let (notes, merge_kind) = crate::timeline::merge_sorted_vecs(&self.notes, new_refs); 140 141 self.notes = notes; 142 let new_items = self.notes.len() - num_prev_items; 143 144 // TODO: technically items could have been added inbetween 145 if new_items > 0 { 146 let mut list = self.list.borrow_mut(); 147 148 match merge_kind { 149 // TODO: update egui_virtual_list to support spliced inserts 150 MergeKind::Spliced => { 151 debug!( 152 "spliced when inserting {} new notes, resetting virtual list", 153 new_refs.len() 154 ); 155 list.reset(); 156 } 157 MergeKind::FrontInsert => { 158 // only run this logic if we're reverse-chronological 159 // reversed in this case means chronological, since the 160 // default is reverse-chronological. yeah it's confusing. 161 if !reversed { 162 debug!("inserting {} new notes at start", new_refs.len()); 163 list.items_inserted_at_start(new_items); 164 } 165 } 166 } 167 } 168 } 169 170 pub fn select_down(&mut self) { 171 debug!("select_down {}", self.selection + 1); 172 if self.selection + 1 > self.notes.len() as i32 { 173 return; 174 } 175 176 self.selection += 1; 177 } 178 179 pub fn select_up(&mut self) { 180 debug!("select_up {}", self.selection - 1); 181 if self.selection - 1 < 0 { 182 return; 183 } 184 185 self.selection -= 1; 186 } 187 } 188 189 /// A column in a deck. Holds navigation state, loaded notes, column kind, etc. 190 #[derive(Debug)] 191 pub struct Timeline { 192 pub kind: TimelineKind, 193 // We may not have the filter loaded yet, so let's make it an option so 194 // that codepaths have to explicitly handle it 195 pub filter: FilterStates, 196 pub views: Vec<TimelineTab>, 197 pub selected_view: usize, 198 199 pub subscription: Option<MultiSubscriber>, 200 } 201 202 impl Timeline { 203 /// Create a timeline from a contact list 204 pub fn contact_list(contact_list: &Note, pubkey: &[u8; 32]) -> Result<Self> { 205 let with_hashtags = false; 206 let filter = filter::filter_from_tags(contact_list, Some(pubkey), with_hashtags)? 207 .into_follow_filter(); 208 209 Ok(Timeline::new( 210 TimelineKind::contact_list(Pubkey::new(*pubkey)), 211 FilterState::ready(filter), 212 TimelineTab::full_tabs(), 213 )) 214 } 215 216 pub fn thread(selection: ThreadSelection) -> Self { 217 let filter = vec![ 218 nostrdb::Filter::new() 219 .kinds([1]) 220 .event(selection.root_id.bytes()) 221 .build(), 222 nostrdb::Filter::new() 223 .ids([selection.root_id.bytes()]) 224 .limit(1) 225 .build(), 226 ]; 227 Timeline::new( 228 TimelineKind::Thread(selection), 229 FilterState::ready(filter), 230 TimelineTab::only_notes_and_replies(), 231 ) 232 } 233 234 pub fn last_per_pubkey(list: &Note, list_kind: &ListKind) -> Result<Self> { 235 let kind = 1; 236 let notes_per_pk = 1; 237 let filter = filter::last_n_per_pubkey_from_tags(list, kind, notes_per_pk)?; 238 239 Ok(Timeline::new( 240 TimelineKind::last_per_pubkey(*list_kind), 241 FilterState::ready(filter), 242 TimelineTab::only_notes_and_replies(), 243 )) 244 } 245 246 pub fn hashtag(hashtag: String) -> Self { 247 let filter = Filter::new() 248 .kinds([1]) 249 .limit(filter::default_limit()) 250 .tags([hashtag.to_lowercase()], 't') 251 .build(); 252 253 Timeline::new( 254 TimelineKind::Hashtag(hashtag), 255 FilterState::ready(vec![filter]), 256 TimelineTab::only_notes_and_replies(), 257 ) 258 } 259 260 pub fn make_view_id(id: &TimelineKind, selected_view: usize) -> egui::Id { 261 egui::Id::new((id, selected_view)) 262 } 263 264 pub fn view_id(&self) -> egui::Id { 265 Timeline::make_view_id(&self.kind, self.selected_view) 266 } 267 268 pub fn new(kind: TimelineKind, filter_state: FilterState, views: Vec<TimelineTab>) -> Self { 269 let filter = FilterStates::new(filter_state); 270 let subscription: Option<MultiSubscriber> = None; 271 let selected_view = 0; 272 273 Timeline { 274 kind, 275 filter, 276 views, 277 subscription, 278 selected_view, 279 } 280 } 281 282 pub fn current_view(&self) -> &TimelineTab { 283 &self.views[self.selected_view] 284 } 285 286 pub fn current_view_mut(&mut self) -> &mut TimelineTab { 287 &mut self.views[self.selected_view] 288 } 289 290 /// Get the note refs for NotesAndReplies. If we only have Notes, then 291 /// just return that instead 292 pub fn all_or_any_notes(&self) -> &[NoteRef] { 293 self.notes(ViewFilter::NotesAndReplies).unwrap_or_else(|| { 294 self.notes(ViewFilter::Notes) 295 .expect("should have at least notes") 296 }) 297 } 298 299 pub fn notes(&self, view: ViewFilter) -> Option<&[NoteRef]> { 300 self.view(view).map(|v| &*v.notes) 301 } 302 303 pub fn view(&self, view: ViewFilter) -> Option<&TimelineTab> { 304 self.views.iter().find(|tab| tab.filter == view) 305 } 306 307 pub fn view_mut(&mut self, view: ViewFilter) -> Option<&mut TimelineTab> { 308 self.views.iter_mut().find(|tab| tab.filter == view) 309 } 310 311 /// Initial insert of notes into a timeline. Subsequent inserts should 312 /// just use the insert function 313 pub fn insert_new( 314 &mut self, 315 txn: &Transaction, 316 ndb: &Ndb, 317 note_cache: &mut NoteCache, 318 notes: &[NoteRef], 319 ) { 320 let filters = { 321 let views = &self.views; 322 let filters: Vec<fn(&CachedNote, &Note) -> bool> = 323 views.iter().map(|v| v.filter.filter()).collect(); 324 filters 325 }; 326 327 for note_ref in notes { 328 for (view, filter) in filters.iter().enumerate() { 329 if let Ok(note) = ndb.get_note_by_key(txn, note_ref.key) { 330 if filter( 331 note_cache.cached_note_or_insert_mut(note_ref.key, ¬e), 332 ¬e, 333 ) { 334 self.views[view].notes.push(*note_ref) 335 } 336 } 337 } 338 } 339 } 340 341 /// The main function used for inserting notes into timelines. Handles 342 /// inserting into multiple views if we have them. All timeline note 343 /// insertions should use this function. 344 pub fn insert( 345 &mut self, 346 new_note_ids: &[NoteKey], 347 ndb: &Ndb, 348 txn: &Transaction, 349 unknown_ids: &mut UnknownIds, 350 note_cache: &mut NoteCache, 351 reversed: bool, 352 ) -> Result<()> { 353 let mut new_refs: Vec<(Note, NoteRef)> = Vec::with_capacity(new_note_ids.len()); 354 355 for key in new_note_ids { 356 let note = if let Ok(note) = ndb.get_note_by_key(txn, *key) { 357 note 358 } else { 359 error!("hit race condition in poll_notes_into_view: https://github.com/damus-io/nostrdb/issues/35 note {:?} was not added to timeline", key); 360 continue; 361 }; 362 363 // Ensure that unknown ids are captured when inserting notes 364 // into the timeline 365 UnknownIds::update_from_note(txn, ndb, unknown_ids, note_cache, ¬e); 366 367 let created_at = note.created_at(); 368 new_refs.push(( 369 note, 370 NoteRef { 371 key: *key, 372 created_at, 373 }, 374 )); 375 } 376 377 for view in &mut self.views { 378 match view.filter { 379 ViewFilter::NotesAndReplies => { 380 let refs: Vec<NoteRef> = new_refs.iter().map(|(_note, nr)| *nr).collect(); 381 382 view.insert(&refs, reversed); 383 } 384 385 ViewFilter::Notes => { 386 let mut filtered_refs = Vec::with_capacity(new_refs.len()); 387 for (note, nr) in &new_refs { 388 let cached_note = note_cache.cached_note_or_insert(nr.key, note); 389 390 if ViewFilter::filter_notes(cached_note, note) { 391 filtered_refs.push(*nr); 392 } 393 } 394 395 view.insert(&filtered_refs, reversed); 396 } 397 } 398 } 399 400 Ok(()) 401 } 402 403 pub fn poll_notes_into_view( 404 &mut self, 405 ndb: &Ndb, 406 txn: &Transaction, 407 unknown_ids: &mut UnknownIds, 408 note_cache: &mut NoteCache, 409 reversed: bool, 410 ) -> Result<()> { 411 if !self.kind.should_subscribe_locally() { 412 // don't need to poll for timelines that don't have local subscriptions 413 return Ok(()); 414 } 415 416 let sub = self 417 .subscription 418 .as_ref() 419 .and_then(|s| s.local_subid) 420 .ok_or(Error::App(notedeck::Error::no_active_sub()))?; 421 422 let new_note_ids = ndb.poll_for_notes(sub, 500); 423 if new_note_ids.is_empty() { 424 return Ok(()); 425 } else { 426 debug!("{} new notes! {:?}", new_note_ids.len(), new_note_ids); 427 } 428 429 self.insert(&new_note_ids, ndb, txn, unknown_ids, note_cache, reversed) 430 } 431 } 432 433 pub enum MergeKind { 434 FrontInsert, 435 Spliced, 436 } 437 438 pub fn merge_sorted_vecs<T: Ord + Copy>(vec1: &[T], vec2: &[T]) -> (Vec<T>, MergeKind) { 439 let mut merged = Vec::with_capacity(vec1.len() + vec2.len()); 440 let mut i = 0; 441 let mut j = 0; 442 let mut result: Option<MergeKind> = None; 443 444 while i < vec1.len() && j < vec2.len() { 445 if vec1[i] <= vec2[j] { 446 if result.is_none() && j < vec2.len() { 447 // if we're pushing from our large list and still have 448 // some left in vec2, then this is a splice 449 result = Some(MergeKind::Spliced); 450 } 451 merged.push(vec1[i]); 452 i += 1; 453 } else { 454 merged.push(vec2[j]); 455 j += 1; 456 } 457 } 458 459 // Append any remaining elements from either vector 460 if i < vec1.len() { 461 merged.extend_from_slice(&vec1[i..]); 462 } 463 if j < vec2.len() { 464 merged.extend_from_slice(&vec2[j..]); 465 } 466 467 (merged, result.unwrap_or(MergeKind::FrontInsert)) 468 } 469 470 /// When adding a new timeline, we may have a situation where the 471 /// FilterState is NeedsRemote. This can happen if we don't yet have the 472 /// contact list, etc. For these situations, we query all of the relays 473 /// with the same sub_id. We keep track of this sub_id and update the 474 /// filter with the latest version of the returned filter (ie contact 475 /// list) when they arrive. 476 /// 477 /// We do this by maintaining this sub_id in the filter state, even when 478 /// in the ready state. See: [`FilterReady`] 479 #[allow(clippy::too_many_arguments)] 480 pub fn setup_new_timeline( 481 timeline: &mut Timeline, 482 ndb: &Ndb, 483 subs: &mut Subscriptions, 484 pool: &mut RelayPool, 485 note_cache: &mut NoteCache, 486 since_optimize: bool, 487 ) { 488 // if we're ready, setup local subs 489 if is_timeline_ready(ndb, pool, note_cache, timeline) { 490 if let Err(err) = setup_timeline_nostrdb_sub(ndb, note_cache, timeline) { 491 error!("setup_new_timeline: {err}"); 492 } 493 } 494 495 for relay in &mut pool.relays { 496 send_initial_timeline_filter(ndb, since_optimize, subs, relay, timeline); 497 } 498 } 499 500 /// Send initial filters for a specific relay. This typically gets called 501 /// when we first connect to a new relay for the first time. For 502 /// situations where you are adding a new timeline, use 503 /// setup_new_timeline. 504 pub fn send_initial_timeline_filters( 505 ndb: &Ndb, 506 since_optimize: bool, 507 timeline_cache: &mut TimelineCache, 508 subs: &mut Subscriptions, 509 pool: &mut RelayPool, 510 relay_id: &str, 511 ) -> Option<()> { 512 info!("Sending initial filters to {}", relay_id); 513 let relay = &mut pool.relays.iter_mut().find(|r| r.url() == relay_id)?; 514 515 for (_kind, timeline) in timeline_cache.timelines.iter_mut() { 516 send_initial_timeline_filter(ndb, since_optimize, subs, relay, timeline); 517 } 518 519 Some(()) 520 } 521 522 pub fn send_initial_timeline_filter( 523 ndb: &Ndb, 524 can_since_optimize: bool, 525 subs: &mut Subscriptions, 526 relay: &mut PoolRelay, 527 timeline: &mut Timeline, 528 ) { 529 let filter_state = timeline.filter.get_mut(relay.url()); 530 531 match filter_state { 532 FilterState::Broken(err) => { 533 error!( 534 "FetchingRemote state in broken state when sending initial timeline filter? {err}" 535 ); 536 } 537 538 FilterState::FetchingRemote(_unisub) => { 539 error!("FetchingRemote state when sending initial timeline filter?"); 540 } 541 542 FilterState::GotRemote(_sub) => { 543 error!("GotRemote state when sending initial timeline filter?"); 544 } 545 546 FilterState::Ready(filter) => { 547 let filter = filter.to_owned(); 548 let new_filters = filter.into_iter().map(|f| { 549 // limit the size of remote filters 550 let default_limit = filter::default_remote_limit(); 551 let mut lim = f.limit().unwrap_or(default_limit); 552 let mut filter = f; 553 if lim > default_limit { 554 lim = default_limit; 555 filter = filter.limit_mut(lim); 556 } 557 558 let notes = timeline.all_or_any_notes(); 559 560 // Should we since optimize? Not always. For example 561 // if we only have a few notes locally. One way to 562 // determine this is by looking at the current filter 563 // and seeing what its limit is. If we have less 564 // notes than the limit, we might want to backfill 565 // older notes 566 if can_since_optimize && filter::should_since_optimize(lim, notes.len()) { 567 filter = filter::since_optimize_filter(filter, notes); 568 } else { 569 warn!("Skipping since optimization for {:?}: number of local notes is less than limit, attempting to backfill.", &timeline.kind); 570 } 571 572 filter 573 }).collect(); 574 575 //let sub_id = damus.gen_subid(&SubKind::Initial); 576 let sub_id = subscriptions::new_sub_id(); 577 subs.subs.insert(sub_id.clone(), SubKind::Initial); 578 579 if let Err(err) = relay.subscribe(sub_id, new_filters) { 580 error!("error subscribing: {err}"); 581 } 582 } 583 584 // we need some data first 585 FilterState::NeedsRemote(filter) => { 586 fetch_contact_list(filter.to_owned(), ndb, subs, relay, timeline) 587 } 588 } 589 } 590 591 fn fetch_contact_list( 592 filter: Vec<Filter>, 593 ndb: &Ndb, 594 subs: &mut Subscriptions, 595 relay: &mut PoolRelay, 596 timeline: &mut Timeline, 597 ) { 598 let sub_kind = SubKind::FetchingContactList(timeline.kind.clone()); 599 let sub_id = subscriptions::new_sub_id(); 600 let local_sub = ndb.subscribe(&filter).expect("sub"); 601 602 timeline.filter.set_relay_state( 603 relay.url().to_string(), 604 FilterState::fetching_remote(sub_id.clone(), local_sub), 605 ); 606 607 subs.subs.insert(sub_id.clone(), sub_kind); 608 609 info!("fetching contact list from {}", relay.url()); 610 if let Err(err) = relay.subscribe(sub_id, filter) { 611 error!("error subscribing: {err}"); 612 } 613 } 614 615 fn setup_initial_timeline( 616 ndb: &Ndb, 617 timeline: &mut Timeline, 618 note_cache: &mut NoteCache, 619 filters: &[Filter], 620 ) -> Result<()> { 621 // some timelines are one-shot and a refreshed, like last_per_pubkey algo feed 622 if timeline.kind.should_subscribe_locally() { 623 let local_sub = ndb.subscribe(filters)?; 624 match &mut timeline.subscription { 625 None => { 626 timeline.subscription = Some(MultiSubscriber::with_initial_local_sub( 627 local_sub, 628 filters.to_vec(), 629 )); 630 } 631 632 Some(msub) => { 633 msub.local_subid = Some(local_sub); 634 } 635 }; 636 } 637 638 debug!( 639 "querying nostrdb sub {:?} {:?}", 640 timeline.subscription, timeline.filter 641 ); 642 643 let mut lim = 0i32; 644 for filter in filters { 645 lim += filter.limit().unwrap_or(1) as i32; 646 } 647 648 let txn = Transaction::new(ndb)?; 649 let notes: Vec<NoteRef> = ndb 650 .query(&txn, filters, lim)? 651 .into_iter() 652 .map(NoteRef::from_query_result) 653 .collect(); 654 655 timeline.insert_new(&txn, ndb, note_cache, ¬es); 656 657 Ok(()) 658 } 659 660 pub fn setup_initial_nostrdb_subs( 661 ndb: &Ndb, 662 note_cache: &mut NoteCache, 663 timeline_cache: &mut TimelineCache, 664 ) -> Result<()> { 665 for (_kind, timeline) in timeline_cache.timelines.iter_mut() { 666 if let Err(err) = setup_timeline_nostrdb_sub(ndb, note_cache, timeline) { 667 error!("setup_initial_nostrdb_subs: {err}"); 668 } 669 } 670 671 Ok(()) 672 } 673 674 fn setup_timeline_nostrdb_sub( 675 ndb: &Ndb, 676 note_cache: &mut NoteCache, 677 timeline: &mut Timeline, 678 ) -> Result<()> { 679 let filter_state = timeline 680 .filter 681 .get_any_ready() 682 .ok_or(Error::App(notedeck::Error::empty_contact_list()))? 683 .to_owned(); 684 685 setup_initial_timeline(ndb, timeline, note_cache, &filter_state)?; 686 687 Ok(()) 688 } 689 690 /// Check our timeline filter and see if we have any filter data ready. 691 /// Our timelines may require additional data before it is functional. For 692 /// example, when we have to fetch a contact list before we do the actual 693 /// following list query. 694 pub fn is_timeline_ready( 695 ndb: &Ndb, 696 pool: &mut RelayPool, 697 note_cache: &mut NoteCache, 698 timeline: &mut Timeline, 699 ) -> bool { 700 // TODO: we should debounce the filter states a bit to make sure we have 701 // seen all of the different contact lists from each relay 702 if let Some(_f) = timeline.filter.get_any_ready() { 703 return true; 704 } 705 706 let (relay_id, sub) = if let Some((relay_id, sub)) = timeline.filter.get_any_gotremote() { 707 (relay_id.to_string(), sub) 708 } else { 709 return false; 710 }; 711 712 // We got at least one eose for our filter request. Let's see 713 // if nostrdb is done processing it yet. 714 let res = ndb.poll_for_notes(sub, 1); 715 if res.is_empty() { 716 debug!( 717 "check_timeline_filter_state: no notes found (yet?) for timeline {:?}", 718 timeline 719 ); 720 return false; 721 } 722 723 info!("notes found for contact timeline after GotRemote!"); 724 725 let note_key = res[0]; 726 let with_hashtags = false; 727 728 let filter = { 729 let txn = Transaction::new(ndb).expect("txn"); 730 let note = ndb.get_note_by_key(&txn, note_key).expect("note"); 731 let add_pk = timeline.kind.pubkey().map(|pk| pk.bytes()); 732 filter::filter_from_tags(¬e, add_pk, with_hashtags).map(|f| f.into_follow_filter()) 733 }; 734 735 // TODO: into_follow_filter is hardcoded to contact lists, let's generalize 736 match filter { 737 Err(notedeck::Error::Filter(e)) => { 738 error!("got broken when building filter {e}"); 739 timeline 740 .filter 741 .set_relay_state(relay_id, FilterState::broken(e)); 742 false 743 } 744 Err(err) => { 745 error!("got broken when building filter {err}"); 746 timeline 747 .filter 748 .set_relay_state(relay_id, FilterState::broken(FilterError::EmptyContactList)); 749 false 750 } 751 Ok(filter) => { 752 // we just switched to the ready state, we should send initial 753 // queries and setup the local subscription 754 info!("Found contact list! Setting up local and remote contact list query"); 755 setup_initial_timeline(ndb, timeline, note_cache, &filter).expect("setup init"); 756 timeline 757 .filter 758 .set_relay_state(relay_id, FilterState::ready(filter.clone())); 759 760 //let ck = &timeline.kind; 761 //let subid = damus.gen_subid(&SubKind::Column(ck.clone())); 762 let subid = subscriptions::new_sub_id(); 763 pool.subscribe(subid, filter); 764 true 765 } 766 } 767 }