kind.rs (23986B)
1 use crate::{ 2 error::Error, 3 search::SearchQuery, 4 timeline::{Timeline, TimelineTab}, 5 }; 6 use enostr::{Filter, NoteId, Pubkey}; 7 use nostrdb::{Ndb, Transaction}; 8 use notedeck::{ 9 filter::{self, default_limit}, 10 FilterError, FilterState, NoteCache, RootIdError, RootNoteIdBuf, 11 }; 12 use serde::{Deserialize, Serialize}; 13 use std::hash::{Hash, Hasher}; 14 use std::{borrow::Cow, fmt::Display}; 15 use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter}; 16 use tracing::{error, warn}; 17 18 #[derive(Clone, Hash, Copy, Default, Debug, PartialEq, Eq, Serialize, Deserialize)] 19 pub enum PubkeySource { 20 Explicit(Pubkey), 21 #[default] 22 DeckAuthor, 23 } 24 25 #[derive(Debug, Clone, Copy, PartialEq, Hash, Eq)] 26 pub enum ListKind { 27 Contact(Pubkey), 28 } 29 30 impl ListKind { 31 pub fn pubkey(&self) -> Option<&Pubkey> { 32 match self { 33 Self::Contact(pk) => Some(pk), 34 } 35 } 36 } 37 38 impl PubkeySource { 39 pub fn pubkey(pubkey: Pubkey) -> Self { 40 PubkeySource::Explicit(pubkey) 41 } 42 43 pub fn as_pubkey<'a>(&'a self, deck_author: &'a Pubkey) -> &'a Pubkey { 44 match self { 45 PubkeySource::Explicit(pk) => pk, 46 PubkeySource::DeckAuthor => deck_author, 47 } 48 } 49 } 50 51 impl TokenSerializable for PubkeySource { 52 fn serialize_tokens(&self, writer: &mut TokenWriter) { 53 match self { 54 PubkeySource::DeckAuthor => { 55 writer.write_token("deck_author"); 56 } 57 PubkeySource::Explicit(pk) => { 58 writer.write_token(&hex::encode(pk.bytes())); 59 } 60 } 61 } 62 63 fn parse_from_tokens<'a>(parser: &mut TokenParser<'a>) -> Result<Self, ParseError<'a>> { 64 parser.try_parse(|p| { 65 match p.pull_token() { 66 // we handle bare payloads and assume they are explicit pubkey sources 67 Ok("explicit") => { 68 if let Ok(hex) = p.pull_token() { 69 let pk = Pubkey::from_hex(hex).map_err(|_| ParseError::HexDecodeFailed)?; 70 Ok(PubkeySource::Explicit(pk)) 71 } else { 72 Err(ParseError::HexDecodeFailed) 73 } 74 } 75 76 Err(_) | Ok("deck_author") => Ok(PubkeySource::DeckAuthor), 77 78 Ok(hex) => { 79 let pk = Pubkey::from_hex(hex).map_err(|_| ParseError::HexDecodeFailed)?; 80 Ok(PubkeySource::Explicit(pk)) 81 } 82 } 83 }) 84 } 85 } 86 87 impl ListKind { 88 pub fn contact_list(pk: Pubkey) -> Self { 89 ListKind::Contact(pk) 90 } 91 92 pub fn parse<'a>( 93 parser: &mut TokenParser<'a>, 94 deck_author: &Pubkey, 95 ) -> Result<Self, ParseError<'a>> { 96 parser.parse_all(|p| { 97 p.parse_token("contact")?; 98 let pk_src = PubkeySource::parse_from_tokens(p)?; 99 Ok(ListKind::Contact(*pk_src.as_pubkey(deck_author))) 100 }) 101 102 /* here for u when you need more things to parse 103 TokenParser::alt( 104 parser, 105 &[|p| { 106 p.parse_all(|p| { 107 p.parse_token("contact")?; 108 let pk_src = PubkeySource::parse_from_tokens(p)?; 109 Ok(ListKind::Contact(pk_src)) 110 }); 111 },|p| { 112 // more cases... 113 }], 114 ) 115 */ 116 } 117 118 pub fn serialize_tokens(&self, writer: &mut TokenWriter) { 119 match self { 120 ListKind::Contact(pk) => { 121 writer.write_token("contact"); 122 PubkeySource::pubkey(*pk).serialize_tokens(writer); 123 } 124 } 125 } 126 } 127 128 /// Thread selection hashing is done in a specific way. For TimelineCache 129 /// lookups, we want to only let the root_id influence thread selection. 130 /// This way Thread TimelineKinds always map to the same cached timeline 131 /// for now (we will likely have to rework this since threads aren't 132 /// *really* timelines) 133 #[derive(Debug, Clone)] 134 pub struct ThreadSelection { 135 pub root_id: RootNoteIdBuf, 136 137 /// The selected note, if different than the root_id. None here 138 /// means the root is selected 139 pub selected_note: Option<NoteId>, 140 } 141 142 impl ThreadSelection { 143 pub fn selected_or_root(&self) -> &[u8; 32] { 144 self.selected_note 145 .as_ref() 146 .map(|sn| sn.bytes()) 147 .unwrap_or(self.root_id.bytes()) 148 } 149 150 pub fn from_root_id(root_id: RootNoteIdBuf) -> Self { 151 Self { 152 root_id, 153 selected_note: None, 154 } 155 } 156 157 pub fn from_note_id( 158 ndb: &Ndb, 159 note_cache: &mut NoteCache, 160 txn: &Transaction, 161 note_id: NoteId, 162 ) -> Result<Self, RootIdError> { 163 let root_id = RootNoteIdBuf::new(ndb, note_cache, txn, note_id.bytes())?; 164 Ok(if root_id.bytes() == note_id.bytes() { 165 Self::from_root_id(root_id) 166 } else { 167 Self { 168 root_id, 169 selected_note: Some(note_id), 170 } 171 }) 172 } 173 } 174 175 impl Hash for ThreadSelection { 176 fn hash<H: Hasher>(&self, state: &mut H) { 177 // only hash the root id for thread selection 178 self.root_id.hash(state) 179 } 180 } 181 182 // need this to only match root_id or else hash lookups will fail 183 impl PartialEq for ThreadSelection { 184 fn eq(&self, other: &Self) -> bool { 185 self.root_id == other.root_id 186 } 187 } 188 189 impl Eq for ThreadSelection {} 190 191 /// 192 /// What kind of timeline is it? 193 /// - Follow List 194 /// - Notifications 195 /// - DM 196 /// - filter 197 /// - ... etc 198 /// 199 #[derive(Debug, Clone, PartialEq, Eq, Hash)] 200 pub enum TimelineKind { 201 List(ListKind), 202 203 Search(SearchQuery), 204 205 /// The last not per pubkey 206 Algo(AlgoTimeline), 207 208 Notifications(Pubkey), 209 210 Profile(Pubkey), 211 212 Thread(ThreadSelection), 213 214 Universe, 215 216 /// Generic filter, references a hash of a filter 217 Generic(u64), 218 219 Hashtag(String), 220 } 221 222 const NOTIFS_TOKEN_DEPRECATED: &str = "notifs"; 223 const NOTIFS_TOKEN: &str = "notifications"; 224 225 /// Hardcoded algo timelines 226 #[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)] 227 pub enum AlgoTimeline { 228 /// LastPerPubkey: a special nostr query that fetches the last N 229 /// notes for each pubkey on the list 230 LastPerPubkey(ListKind), 231 } 232 233 /// The identifier for our last per pubkey algo 234 const LAST_PER_PUBKEY_TOKEN: &str = "last_per_pubkey"; 235 236 impl AlgoTimeline { 237 pub fn serialize_tokens(&self, writer: &mut TokenWriter) { 238 match self { 239 AlgoTimeline::LastPerPubkey(list_kind) => { 240 writer.write_token(LAST_PER_PUBKEY_TOKEN); 241 list_kind.serialize_tokens(writer); 242 } 243 } 244 } 245 246 pub fn parse<'a>( 247 parser: &mut TokenParser<'a>, 248 deck_author: &Pubkey, 249 ) -> Result<Self, ParseError<'a>> { 250 parser.parse_all(|p| { 251 p.parse_token(LAST_PER_PUBKEY_TOKEN)?; 252 Ok(AlgoTimeline::LastPerPubkey(ListKind::parse( 253 p, 254 deck_author, 255 )?)) 256 }) 257 } 258 } 259 260 impl Display for TimelineKind { 261 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 262 match self { 263 TimelineKind::List(ListKind::Contact(_src)) => f.write_str("Contacts"), 264 TimelineKind::Algo(AlgoTimeline::LastPerPubkey(_lk)) => f.write_str("Last Notes"), 265 TimelineKind::Generic(_) => f.write_str("Timeline"), 266 TimelineKind::Notifications(_) => f.write_str("Notifications"), 267 TimelineKind::Profile(_) => f.write_str("Profile"), 268 TimelineKind::Universe => f.write_str("Universe"), 269 TimelineKind::Hashtag(_) => f.write_str("Hashtag"), 270 TimelineKind::Thread(_) => f.write_str("Thread"), 271 TimelineKind::Search(_) => f.write_str("Search"), 272 } 273 } 274 } 275 276 impl TimelineKind { 277 pub fn pubkey(&self) -> Option<&Pubkey> { 278 match self { 279 TimelineKind::List(list_kind) => list_kind.pubkey(), 280 TimelineKind::Algo(AlgoTimeline::LastPerPubkey(list_kind)) => list_kind.pubkey(), 281 TimelineKind::Notifications(pk) => Some(pk), 282 TimelineKind::Profile(pk) => Some(pk), 283 TimelineKind::Universe => None, 284 TimelineKind::Generic(_) => None, 285 TimelineKind::Hashtag(_ht) => None, 286 TimelineKind::Thread(_ht) => None, 287 TimelineKind::Search(query) => query.author(), 288 } 289 } 290 291 /// Some feeds are not realtime, like certain algo feeds 292 pub fn should_subscribe_locally(&self) -> bool { 293 match self { 294 TimelineKind::Algo(AlgoTimeline::LastPerPubkey(_list_kind)) => false, 295 296 TimelineKind::List(_list_kind) => true, 297 TimelineKind::Notifications(_pk_src) => true, 298 TimelineKind::Profile(_pk_src) => true, 299 TimelineKind::Universe => true, 300 TimelineKind::Generic(_) => true, 301 TimelineKind::Hashtag(_ht) => true, 302 TimelineKind::Thread(_ht) => true, 303 TimelineKind::Search(_q) => true, 304 } 305 } 306 307 // NOTE!!: if you just added a TimelineKind enum, make sure to update 308 // the parser below as well 309 pub fn serialize_tokens(&self, writer: &mut TokenWriter) { 310 match self { 311 TimelineKind::Search(query) => { 312 writer.write_token("search"); 313 query.serialize_tokens(writer) 314 } 315 TimelineKind::List(list_kind) => list_kind.serialize_tokens(writer), 316 TimelineKind::Algo(algo_timeline) => algo_timeline.serialize_tokens(writer), 317 TimelineKind::Notifications(pk) => { 318 writer.write_token(NOTIFS_TOKEN); 319 PubkeySource::pubkey(*pk).serialize_tokens(writer); 320 } 321 TimelineKind::Profile(pk) => { 322 writer.write_token("profile"); 323 PubkeySource::pubkey(*pk).serialize_tokens(writer); 324 } 325 TimelineKind::Thread(root_note_id) => { 326 writer.write_token("thread"); 327 writer.write_token(&root_note_id.root_id.hex()); 328 } 329 TimelineKind::Universe => { 330 writer.write_token("universe"); 331 } 332 TimelineKind::Generic(_usize) => { 333 // TODO: lookup filter and then serialize 334 writer.write_token("generic"); 335 } 336 TimelineKind::Hashtag(ht) => { 337 writer.write_token("hashtag"); 338 writer.write_token(ht); 339 } 340 } 341 } 342 343 pub fn parse<'a>( 344 parser: &mut TokenParser<'a>, 345 deck_author: &Pubkey, 346 ) -> Result<Self, ParseError<'a>> { 347 let profile = parser.try_parse(|p| { 348 p.parse_token("profile")?; 349 let pk_src = PubkeySource::parse_from_tokens(p)?; 350 Ok(TimelineKind::Profile(*pk_src.as_pubkey(deck_author))) 351 }); 352 if profile.is_ok() { 353 return profile; 354 } 355 356 let notifications = parser.try_parse(|p| { 357 // still handle deprecated form (notifs) 358 p.parse_any_token(&[NOTIFS_TOKEN, NOTIFS_TOKEN_DEPRECATED])?; 359 let pk_src = PubkeySource::parse_from_tokens(p)?; 360 Ok(TimelineKind::Notifications(*pk_src.as_pubkey(deck_author))) 361 }); 362 if notifications.is_ok() { 363 return notifications; 364 } 365 366 let list_tl = 367 parser.try_parse(|p| Ok(TimelineKind::List(ListKind::parse(p, deck_author)?))); 368 if list_tl.is_ok() { 369 return list_tl; 370 } 371 372 let algo_tl = 373 parser.try_parse(|p| Ok(TimelineKind::Algo(AlgoTimeline::parse(p, deck_author)?))); 374 if algo_tl.is_ok() { 375 return algo_tl; 376 } 377 378 TokenParser::alt( 379 parser, 380 &[ 381 |p| { 382 p.parse_token("thread")?; 383 Ok(TimelineKind::Thread(ThreadSelection::from_root_id( 384 RootNoteIdBuf::new_unsafe(tokenator::parse_hex_id(p)?), 385 ))) 386 }, 387 |p| { 388 p.parse_token("universe")?; 389 Ok(TimelineKind::Universe) 390 }, 391 |p| { 392 p.parse_token("generic")?; 393 // TODO: generic filter serialization 394 Ok(TimelineKind::Generic(0)) 395 }, 396 |p| { 397 p.parse_token("hashtag")?; 398 Ok(TimelineKind::Hashtag(p.pull_token()?.to_string())) 399 }, 400 |p| { 401 p.parse_token("search")?; 402 let search_query = SearchQuery::parse_from_tokens(p)?; 403 Ok(TimelineKind::Search(search_query)) 404 }, 405 ], 406 ) 407 } 408 409 pub fn last_per_pubkey(list_kind: ListKind) -> Self { 410 TimelineKind::Algo(AlgoTimeline::LastPerPubkey(list_kind)) 411 } 412 413 pub fn contact_list(pk: Pubkey) -> Self { 414 TimelineKind::List(ListKind::contact_list(pk)) 415 } 416 417 pub fn search(s: String) -> Self { 418 TimelineKind::Search(SearchQuery::new(s)) 419 } 420 421 pub fn is_contacts(&self) -> bool { 422 matches!(self, TimelineKind::List(ListKind::Contact(_))) 423 } 424 425 pub fn profile(pk: Pubkey) -> Self { 426 TimelineKind::Profile(pk) 427 } 428 429 pub fn thread(selected_note: ThreadSelection) -> Self { 430 TimelineKind::Thread(selected_note) 431 } 432 433 pub fn is_notifications(&self) -> bool { 434 matches!(self, TimelineKind::Notifications(_)) 435 } 436 437 pub fn notifications(pk: Pubkey) -> Self { 438 TimelineKind::Notifications(pk) 439 } 440 441 // TODO: probably should set default limit here 442 pub fn filters(&self, txn: &Transaction, ndb: &Ndb) -> FilterState { 443 match self { 444 TimelineKind::Search(s) => FilterState::ready(search_filter(s)), 445 446 TimelineKind::Universe => FilterState::ready(universe_filter()), 447 448 TimelineKind::List(list_k) => match list_k { 449 ListKind::Contact(pubkey) => contact_filter_state(txn, ndb, pubkey), 450 }, 451 452 // TODO: still need to update this to fetch likes, zaps, etc 453 TimelineKind::Notifications(pubkey) => FilterState::ready(vec![Filter::new() 454 .pubkeys([pubkey.bytes()]) 455 .kinds([1]) 456 .limit(default_limit()) 457 .build()]), 458 459 TimelineKind::Hashtag(hashtag) => FilterState::ready(vec![Filter::new() 460 .kinds([1]) 461 .limit(filter::default_limit()) 462 .tags([hashtag.to_lowercase()], 't') 463 .build()]), 464 465 TimelineKind::Algo(algo_timeline) => match algo_timeline { 466 AlgoTimeline::LastPerPubkey(list_k) => match list_k { 467 ListKind::Contact(pubkey) => last_per_pubkey_filter_state(ndb, pubkey), 468 }, 469 }, 470 471 TimelineKind::Generic(_) => { 472 todo!("implement generic filter lookups") 473 } 474 475 TimelineKind::Thread(selection) => FilterState::ready(vec![ 476 nostrdb::Filter::new() 477 .kinds([1]) 478 .event(selection.root_id.bytes()) 479 .build(), 480 nostrdb::Filter::new() 481 .ids([selection.root_id.bytes()]) 482 .limit(1) 483 .build(), 484 ]), 485 486 TimelineKind::Profile(pk) => FilterState::ready(vec![Filter::new() 487 .authors([pk.bytes()]) 488 .kinds([1]) 489 .limit(default_limit()) 490 .build()]), 491 } 492 } 493 494 pub fn into_timeline(self, txn: &Transaction, ndb: &Ndb) -> Option<Timeline> { 495 match self { 496 TimelineKind::Search(s) => { 497 let filter = FilterState::ready(search_filter(&s)); 498 Some(Timeline::new( 499 TimelineKind::Search(s), 500 filter, 501 TimelineTab::full_tabs(), 502 )) 503 } 504 505 TimelineKind::Universe => Some(Timeline::new( 506 TimelineKind::Universe, 507 FilterState::ready(universe_filter()), 508 TimelineTab::no_replies(), 509 )), 510 511 TimelineKind::Thread(root_id) => Some(Timeline::thread(root_id)), 512 513 TimelineKind::Generic(_filter_id) => { 514 warn!("you can't convert a TimelineKind::Generic to a Timeline"); 515 // TODO: you actually can! just need to look up the filter id 516 None 517 } 518 519 TimelineKind::Algo(AlgoTimeline::LastPerPubkey(ListKind::Contact(pk))) => { 520 let contact_filter = Filter::new() 521 .authors([pk.bytes()]) 522 .kinds([3]) 523 .limit(1) 524 .build(); 525 526 let results = ndb 527 .query(txn, &[contact_filter.clone()], 1) 528 .expect("contact query failed?"); 529 530 let kind_fn = TimelineKind::last_per_pubkey; 531 let tabs = TimelineTab::only_notes_and_replies(); 532 533 if results.is_empty() { 534 return Some(Timeline::new( 535 kind_fn(ListKind::contact_list(pk)), 536 FilterState::needs_remote(vec![contact_filter.clone()]), 537 tabs, 538 )); 539 } 540 541 let list_kind = ListKind::contact_list(pk); 542 543 match Timeline::last_per_pubkey(&results[0].note, &list_kind) { 544 Err(Error::App(notedeck::Error::Filter(FilterError::EmptyContactList))) => { 545 Some(Timeline::new( 546 kind_fn(list_kind), 547 FilterState::needs_remote(vec![contact_filter]), 548 tabs, 549 )) 550 } 551 Err(e) => { 552 error!("Unexpected error: {e}"); 553 None 554 } 555 Ok(tl) => Some(tl), 556 } 557 } 558 559 TimelineKind::Profile(pk) => { 560 let filter = Filter::new() 561 .authors([pk.bytes()]) 562 .kinds([1]) 563 .limit(default_limit()) 564 .build(); 565 566 Some(Timeline::new( 567 TimelineKind::profile(pk), 568 FilterState::ready(vec![filter]), 569 TimelineTab::full_tabs(), 570 )) 571 } 572 573 TimelineKind::Notifications(pk) => { 574 let notifications_filter = Filter::new() 575 .pubkeys([pk.bytes()]) 576 .kinds([1]) 577 .limit(default_limit()) 578 .build(); 579 580 Some(Timeline::new( 581 TimelineKind::notifications(pk), 582 FilterState::ready(vec![notifications_filter]), 583 TimelineTab::only_notes_and_replies(), 584 )) 585 } 586 587 TimelineKind::Hashtag(hashtag) => Some(Timeline::hashtag(hashtag)), 588 589 TimelineKind::List(ListKind::Contact(pk)) => Some(Timeline::new( 590 TimelineKind::contact_list(pk), 591 contact_filter_state(txn, ndb, &pk), 592 TimelineTab::full_tabs(), 593 )), 594 } 595 } 596 597 pub fn to_title(&self) -> ColumnTitle<'_> { 598 match self { 599 TimelineKind::Search(query) => { 600 ColumnTitle::formatted(format!("Search \"{}\"", query.search)) 601 } 602 TimelineKind::List(list_kind) => match list_kind { 603 ListKind::Contact(_pubkey_source) => ColumnTitle::simple("Contacts"), 604 }, 605 TimelineKind::Algo(AlgoTimeline::LastPerPubkey(list_kind)) => match list_kind { 606 ListKind::Contact(_pubkey_source) => ColumnTitle::simple("Contacts (last notes)"), 607 }, 608 TimelineKind::Notifications(_pubkey_source) => ColumnTitle::simple("Notifications"), 609 TimelineKind::Profile(_pubkey_source) => ColumnTitle::needs_db(self), 610 TimelineKind::Thread(_root_id) => ColumnTitle::simple("Thread"), 611 TimelineKind::Universe => ColumnTitle::simple("Universe"), 612 TimelineKind::Generic(_) => ColumnTitle::simple("Custom"), 613 TimelineKind::Hashtag(hashtag) => ColumnTitle::formatted(hashtag.to_string()), 614 } 615 } 616 } 617 618 #[derive(Debug)] 619 pub struct TitleNeedsDb<'a> { 620 kind: &'a TimelineKind, 621 } 622 623 impl<'a> TitleNeedsDb<'a> { 624 pub fn new(kind: &'a TimelineKind) -> Self { 625 TitleNeedsDb { kind } 626 } 627 628 pub fn title<'txn>(&self, txn: &'txn Transaction, ndb: &Ndb) -> &'txn str { 629 if let TimelineKind::Profile(pubkey) = self.kind { 630 let profile = ndb.get_profile_by_pubkey(txn, pubkey); 631 let m_name = profile 632 .as_ref() 633 .ok() 634 .map(|p| crate::profile::get_display_name(Some(p)).name()); 635 636 m_name.unwrap_or("Profile") 637 } else { 638 "Unknown" 639 } 640 } 641 } 642 643 /// This saves us from having to construct a transaction if we don't need to 644 /// for a particular column when rendering the title 645 #[derive(Debug)] 646 pub enum ColumnTitle<'a> { 647 Simple(Cow<'static, str>), 648 NeedsDb(TitleNeedsDb<'a>), 649 } 650 651 impl<'a> ColumnTitle<'a> { 652 pub fn simple(title: &'static str) -> Self { 653 Self::Simple(Cow::Borrowed(title)) 654 } 655 656 pub fn formatted(title: String) -> Self { 657 Self::Simple(Cow::Owned(title)) 658 } 659 660 pub fn needs_db(kind: &'a TimelineKind) -> ColumnTitle<'a> { 661 Self::NeedsDb(TitleNeedsDb::new(kind)) 662 } 663 } 664 665 fn contact_filter_state(txn: &Transaction, ndb: &Ndb, pk: &Pubkey) -> FilterState { 666 let contact_filter = Filter::new() 667 .authors([pk.bytes()]) 668 .kinds([3]) 669 .limit(1) 670 .build(); 671 672 let results = ndb 673 .query(txn, &[contact_filter.clone()], 1) 674 .expect("contact query failed?"); 675 676 if results.is_empty() { 677 FilterState::needs_remote(vec![contact_filter.clone()]) 678 } else { 679 let with_hashtags = false; 680 match filter::filter_from_tags(&results[0].note, Some(pk.bytes()), with_hashtags) { 681 Err(notedeck::Error::Filter(FilterError::EmptyContactList)) => { 682 FilterState::needs_remote(vec![contact_filter]) 683 } 684 Err(err) => { 685 error!("Error getting contact filter state: {err}"); 686 FilterState::Broken(FilterError::EmptyContactList) 687 } 688 Ok(filter) => FilterState::ready(filter.into_follow_filter()), 689 } 690 } 691 } 692 693 fn last_per_pubkey_filter_state(ndb: &Ndb, pk: &Pubkey) -> FilterState { 694 let contact_filter = Filter::new() 695 .authors([pk.bytes()]) 696 .kinds([3]) 697 .limit(1) 698 .build(); 699 700 let txn = Transaction::new(ndb).expect("txn"); 701 let results = ndb 702 .query(&txn, &[contact_filter.clone()], 1) 703 .expect("contact query failed?"); 704 705 if results.is_empty() { 706 FilterState::needs_remote(vec![contact_filter]) 707 } else { 708 let kind = 1; 709 let notes_per_pk = 1; 710 match filter::last_n_per_pubkey_from_tags(&results[0].note, kind, notes_per_pk) { 711 Err(notedeck::Error::Filter(FilterError::EmptyContactList)) => { 712 FilterState::needs_remote(vec![contact_filter]) 713 } 714 Err(err) => { 715 error!("Error getting contact filter state: {err}"); 716 FilterState::Broken(FilterError::EmptyContactList) 717 } 718 Ok(filter) => FilterState::ready(filter), 719 } 720 } 721 } 722 723 fn search_filter(s: &SearchQuery) -> Vec<Filter> { 724 vec![s.filter().limit(default_limit()).build()] 725 } 726 727 fn universe_filter() -> Vec<Filter> { 728 vec![Filter::new().kinds([1]).limit(default_limit()).build()] 729 }