kind.rs (24969B)
1 use crate::error::Error; 2 use crate::search::SearchQuery; 3 use crate::timeline::{Timeline, TimelineTab}; 4 use enostr::{Filter, NoteId, Pubkey}; 5 use nostrdb::{Ndb, Transaction}; 6 use notedeck::filter::{NdbQueryPackage, ValidKind}; 7 use notedeck::{ 8 contacts::{contacts_filter, hybrid_contacts_filter}, 9 filter::{self, default_limit, default_remote_limit, HybridFilter}, 10 tr, FilterError, FilterState, Localization, NoteCache, RootIdError, RootNoteIdBuf, 11 }; 12 use serde::{Deserialize, Serialize}; 13 use std::borrow::Cow; 14 use std::hash::{Hash, Hasher}; 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 #[derive(Debug, Clone)] 129 pub struct ThreadSelection { 130 pub root_id: RootNoteIdBuf, 131 132 /// The selected note, if different than the root_id. None here 133 /// means the root is selected 134 pub selected_note: Option<NoteId>, 135 } 136 137 impl ThreadSelection { 138 pub fn selected_or_root(&self) -> &[u8; 32] { 139 self.selected_note 140 .as_ref() 141 .map(|sn| sn.bytes()) 142 .unwrap_or(self.root_id.bytes()) 143 } 144 145 pub fn from_root_id(root_id: RootNoteIdBuf) -> Self { 146 Self { 147 root_id, 148 selected_note: None, 149 } 150 } 151 152 pub fn from_note_id( 153 ndb: &Ndb, 154 note_cache: &mut NoteCache, 155 txn: &Transaction, 156 note_id: NoteId, 157 ) -> Result<Self, RootIdError> { 158 let root_id = RootNoteIdBuf::new(ndb, note_cache, txn, note_id.bytes())?; 159 Ok(if root_id.bytes() == note_id.bytes() { 160 Self::from_root_id(root_id) 161 } else { 162 Self { 163 root_id, 164 selected_note: Some(note_id), 165 } 166 }) 167 } 168 } 169 170 /// Thread selection hashing is done in a specific way. For TimelineCache 171 /// lookups, we want to only let the root_id influence thread selection. 172 /// This way Thread TimelineKinds always map to the same cached timeline 173 /// for now (we will likely have to rework this since threads aren't 174 /// *really* timelines) 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 Universe, 213 214 /// Generic filter, references a hash of a filter 215 Generic(u64), 216 217 Hashtag(Vec<String>), 218 } 219 220 const NOTIFS_TOKEN_DEPRECATED: &str = "notifs"; 221 const NOTIFS_TOKEN: &str = "notifications"; 222 223 /// Hardcoded algo timelines 224 #[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)] 225 pub enum AlgoTimeline { 226 /// LastPerPubkey: a special nostr query that fetches the last N 227 /// notes for each pubkey on the list 228 LastPerPubkey(ListKind), 229 } 230 231 /// The identifier for our last per pubkey algo 232 const LAST_PER_PUBKEY_TOKEN: &str = "last_per_pubkey"; 233 234 impl AlgoTimeline { 235 pub fn serialize_tokens(&self, writer: &mut TokenWriter) { 236 match self { 237 AlgoTimeline::LastPerPubkey(list_kind) => { 238 writer.write_token(LAST_PER_PUBKEY_TOKEN); 239 list_kind.serialize_tokens(writer); 240 } 241 } 242 } 243 244 pub fn parse<'a>( 245 parser: &mut TokenParser<'a>, 246 deck_author: &Pubkey, 247 ) -> Result<Self, ParseError<'a>> { 248 parser.parse_all(|p| { 249 p.parse_token(LAST_PER_PUBKEY_TOKEN)?; 250 Ok(AlgoTimeline::LastPerPubkey(ListKind::parse( 251 p, 252 deck_author, 253 )?)) 254 }) 255 } 256 } 257 258 /* 259 impl Display for TimelineKind { 260 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 261 match self { 262 TimelineKind::List(ListKind::Contact(_src)) => write!( 263 f, 264 "{}", 265 tr!("Home", "Timeline kind label for contact lists") 266 ), 267 TimelineKind::Algo(AlgoTimeline::LastPerPubkey(_lk)) => write!( 268 f, 269 "{}", 270 tr!( 271 "Last Notes", 272 "Timeline kind label for last notes per pubkey" 273 ) 274 ), 275 TimelineKind::Generic(_) => { 276 write!(f, "{}", tr!("Timeline", "Generic timeline kind label")) 277 } 278 TimelineKind::Notifications(_) => write!( 279 f, 280 "{}", 281 tr!("Notifications", "Timeline kind label for notifications") 282 ), 283 TimelineKind::Profile(_) => write!( 284 f, 285 "{}", 286 tr!("Profile", "Timeline kind label for user profiles") 287 ), 288 TimelineKind::Universe => write!( 289 f, 290 "{}", 291 tr!("Universe", "Timeline kind label for universe feed") 292 ), 293 TimelineKind::Hashtag(_) => write!( 294 f, 295 "{}", 296 tr!("Hashtag", "Timeline kind label for hashtag feeds") 297 ), 298 TimelineKind::Search(_) => write!( 299 f, 300 "{}", 301 tr!("Search", "Timeline kind label for search results") 302 ), 303 } 304 } 305 } 306 */ 307 308 impl TimelineKind { 309 pub fn pubkey(&self) -> Option<&Pubkey> { 310 match self { 311 TimelineKind::List(list_kind) => list_kind.pubkey(), 312 TimelineKind::Algo(AlgoTimeline::LastPerPubkey(list_kind)) => list_kind.pubkey(), 313 TimelineKind::Notifications(pk) => Some(pk), 314 TimelineKind::Profile(pk) => Some(pk), 315 TimelineKind::Universe => None, 316 TimelineKind::Generic(_) => None, 317 TimelineKind::Hashtag(_ht) => None, 318 TimelineKind::Search(query) => query.author(), 319 } 320 } 321 322 /// Some feeds are not realtime, like certain algo feeds 323 pub fn should_subscribe_locally(&self) -> bool { 324 match self { 325 TimelineKind::Algo(AlgoTimeline::LastPerPubkey(_list_kind)) => false, 326 327 TimelineKind::List(_list_kind) => true, 328 TimelineKind::Notifications(_pk_src) => true, 329 TimelineKind::Profile(_pk_src) => true, 330 TimelineKind::Universe => true, 331 TimelineKind::Generic(_) => true, 332 TimelineKind::Hashtag(_ht) => true, 333 TimelineKind::Search(_q) => true, 334 } 335 } 336 337 // NOTE!!: if you just added a TimelineKind enum, make sure to update 338 // the parser below as well 339 pub fn serialize_tokens(&self, writer: &mut TokenWriter) { 340 match self { 341 TimelineKind::Search(query) => { 342 writer.write_token("search"); 343 query.serialize_tokens(writer) 344 } 345 TimelineKind::List(list_kind) => list_kind.serialize_tokens(writer), 346 TimelineKind::Algo(algo_timeline) => algo_timeline.serialize_tokens(writer), 347 TimelineKind::Notifications(pk) => { 348 writer.write_token(NOTIFS_TOKEN); 349 PubkeySource::pubkey(*pk).serialize_tokens(writer); 350 } 351 TimelineKind::Profile(pk) => { 352 writer.write_token("profile"); 353 PubkeySource::pubkey(*pk).serialize_tokens(writer); 354 } 355 TimelineKind::Universe => { 356 writer.write_token("universe"); 357 } 358 TimelineKind::Generic(_usize) => { 359 // TODO: lookup filter and then serialize 360 writer.write_token("generic"); 361 } 362 TimelineKind::Hashtag(ht) => { 363 writer.write_token("hashtag"); 364 writer.write_token(&ht.join(" ")); 365 } 366 } 367 } 368 369 pub fn parse<'a>( 370 parser: &mut TokenParser<'a>, 371 deck_author: &Pubkey, 372 ) -> Result<Self, ParseError<'a>> { 373 let profile = parser.try_parse(|p| { 374 p.parse_token("profile")?; 375 let pk_src = PubkeySource::parse_from_tokens(p)?; 376 Ok(TimelineKind::Profile(*pk_src.as_pubkey(deck_author))) 377 }); 378 if profile.is_ok() { 379 return profile; 380 } 381 382 let notifications = parser.try_parse(|p| { 383 // still handle deprecated form (notifs) 384 p.parse_any_token(&[NOTIFS_TOKEN, NOTIFS_TOKEN_DEPRECATED])?; 385 let pk_src = PubkeySource::parse_from_tokens(p)?; 386 Ok(TimelineKind::Notifications(*pk_src.as_pubkey(deck_author))) 387 }); 388 if notifications.is_ok() { 389 return notifications; 390 } 391 392 let list_tl = 393 parser.try_parse(|p| Ok(TimelineKind::List(ListKind::parse(p, deck_author)?))); 394 if list_tl.is_ok() { 395 return list_tl; 396 } 397 398 let algo_tl = 399 parser.try_parse(|p| Ok(TimelineKind::Algo(AlgoTimeline::parse(p, deck_author)?))); 400 if algo_tl.is_ok() { 401 return algo_tl; 402 } 403 404 TokenParser::alt( 405 parser, 406 &[ 407 |p| { 408 p.parse_token("universe")?; 409 Ok(TimelineKind::Universe) 410 }, 411 |p| { 412 p.parse_token("generic")?; 413 // TODO: generic filter serialization 414 Ok(TimelineKind::Generic(0)) 415 }, 416 |p| { 417 p.parse_token("hashtag")?; 418 Ok(TimelineKind::Hashtag( 419 p.pull_token()? 420 .split_whitespace() 421 .filter(|s| !s.is_empty()) 422 .map(|s| s.to_lowercase().to_string()) 423 .collect(), 424 )) 425 }, 426 |p| { 427 p.parse_token("search")?; 428 let search_query = SearchQuery::parse_from_tokens(p)?; 429 Ok(TimelineKind::Search(search_query)) 430 }, 431 ], 432 ) 433 } 434 435 pub fn last_per_pubkey(list_kind: ListKind) -> Self { 436 TimelineKind::Algo(AlgoTimeline::LastPerPubkey(list_kind)) 437 } 438 439 pub fn contact_list(pk: Pubkey) -> Self { 440 TimelineKind::List(ListKind::contact_list(pk)) 441 } 442 443 pub fn search(s: String) -> Self { 444 TimelineKind::Search(SearchQuery::new(s)) 445 } 446 447 pub fn is_contacts(&self) -> bool { 448 matches!(self, TimelineKind::List(ListKind::Contact(_))) 449 } 450 451 pub fn profile(pk: Pubkey) -> Self { 452 TimelineKind::Profile(pk) 453 } 454 455 pub fn is_notifications(&self) -> bool { 456 matches!(self, TimelineKind::Notifications(_)) 457 } 458 459 pub fn notifications(pk: Pubkey) -> Self { 460 TimelineKind::Notifications(pk) 461 } 462 463 // TODO: probably should set default limit here 464 pub fn filters(&self, txn: &Transaction, ndb: &Ndb) -> FilterState { 465 match self { 466 TimelineKind::Search(s) => FilterState::ready(search_filter(s)), 467 468 TimelineKind::Universe => FilterState::ready(universe_filter()), 469 470 TimelineKind::List(list_k) => match list_k { 471 ListKind::Contact(pubkey) => contact_filter_state(txn, ndb, pubkey), 472 }, 473 474 // TODO: still need to update this to fetch likes, zaps, etc 475 TimelineKind::Notifications(pubkey) => { 476 FilterState::ready(vec![notifications_filter(pubkey)]) 477 } 478 479 TimelineKind::Hashtag(hashtag) => { 480 let filters = hashtag 481 .iter() 482 .filter(|tag| !tag.is_empty()) 483 .map(|tag| { 484 Filter::new() 485 .kinds([1]) 486 .limit(filter::default_limit()) 487 .tags([tag.to_lowercase().as_str()], 't') 488 .build() 489 }) 490 .collect::<Vec<_>>(); 491 492 FilterState::ready(filters) 493 } 494 495 TimelineKind::Algo(algo_timeline) => match algo_timeline { 496 AlgoTimeline::LastPerPubkey(list_k) => match list_k { 497 ListKind::Contact(pubkey) => last_per_pubkey_filter_state(ndb, pubkey), 498 }, 499 }, 500 501 TimelineKind::Generic(_) => { 502 todo!("implement generic filter lookups") 503 } 504 505 TimelineKind::Profile(pk) => FilterState::ready_hybrid(profile_filter(pk.bytes())), 506 } 507 } 508 509 pub fn into_timeline(self, txn: &Transaction, ndb: &Ndb) -> Option<Timeline> { 510 match self { 511 TimelineKind::Search(s) => { 512 let filter = FilterState::ready(search_filter(&s)); 513 Some(Timeline::new( 514 TimelineKind::Search(s), 515 filter, 516 TimelineTab::full_tabs(), 517 )) 518 } 519 520 TimelineKind::Universe => Some(Timeline::new( 521 TimelineKind::Universe, 522 FilterState::ready(universe_filter()), 523 TimelineTab::full_tabs(), 524 )), 525 526 TimelineKind::Generic(_filter_id) => { 527 warn!("you can't convert a TimelineKind::Generic to a Timeline"); 528 // TODO: you actually can! just need to look up the filter id 529 None 530 } 531 532 TimelineKind::Algo(AlgoTimeline::LastPerPubkey(ListKind::Contact(pk))) => { 533 let contact_filter = contacts_filter(pk.bytes()); 534 535 let results = ndb 536 .query(txn, std::slice::from_ref(&contact_filter), 1) 537 .expect("contact query failed?"); 538 539 let kind_fn = TimelineKind::last_per_pubkey; 540 let tabs = TimelineTab::only_notes_and_replies(); 541 542 if results.is_empty() { 543 return Some(Timeline::new( 544 kind_fn(ListKind::contact_list(pk)), 545 FilterState::needs_remote(), 546 tabs, 547 )); 548 } 549 550 let list_kind = ListKind::contact_list(pk); 551 552 match Timeline::last_per_pubkey(&results[0].note, &list_kind) { 553 Err(Error::App(notedeck::Error::Filter(FilterError::EmptyContactList))) => { 554 Some(Timeline::new( 555 kind_fn(list_kind), 556 FilterState::needs_remote(), 557 tabs, 558 )) 559 } 560 Err(e) => { 561 error!("Unexpected error: {e}"); 562 None 563 } 564 Ok(tl) => Some(tl), 565 } 566 } 567 568 TimelineKind::Profile(pk) => { 569 let filter = profile_filter(pk.bytes()); 570 Some(Timeline::new( 571 TimelineKind::profile(pk), 572 FilterState::ready_hybrid(filter), 573 TimelineTab::full_tabs(), 574 )) 575 } 576 577 TimelineKind::Notifications(pk) => { 578 let notifications_filter = notifications_filter(&pk); 579 580 Some(Timeline::new( 581 TimelineKind::notifications(pk), 582 FilterState::ready(vec![notifications_filter]), 583 TimelineTab::notifications(), 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, i18n: &mut Localization) -> 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) => { 604 ColumnTitle::formatted(tr!(i18n, "Contacts", "Column title for contact lists")) 605 } 606 }, 607 TimelineKind::Algo(AlgoTimeline::LastPerPubkey(list_kind)) => match list_kind { 608 ListKind::Contact(_pubkey_source) => ColumnTitle::formatted(tr!( 609 i18n, 610 "Contacts (last notes)", 611 "Column title for last notes per contact" 612 )), 613 }, 614 TimelineKind::Notifications(_pubkey_source) => { 615 ColumnTitle::formatted(tr!(i18n, "Notifications", "Column title for notifications")) 616 } 617 TimelineKind::Profile(_pubkey_source) => ColumnTitle::needs_db(self), 618 TimelineKind::Universe => { 619 ColumnTitle::formatted(tr!(i18n, "Universe", "Column title for universe feed")) 620 } 621 TimelineKind::Generic(_) => { 622 ColumnTitle::formatted(tr!(i18n, "Custom", "Column title for custom timelines")) 623 } 624 TimelineKind::Hashtag(hashtag) => ColumnTitle::formatted(hashtag.join(" ").to_string()), 625 } 626 } 627 } 628 629 pub fn notifications_filter(pk: &Pubkey) -> Filter { 630 Filter::new() 631 .pubkeys([pk.bytes()]) 632 .kinds(notification_kinds()) 633 .limit(default_limit()) 634 .build() 635 } 636 637 pub fn notification_kinds() -> [u64; 3] { 638 [1, 7, 6] 639 } 640 641 #[derive(Debug)] 642 pub struct TitleNeedsDb<'a> { 643 kind: &'a TimelineKind, 644 } 645 646 impl<'a> TitleNeedsDb<'a> { 647 pub fn new(kind: &'a TimelineKind) -> Self { 648 TitleNeedsDb { kind } 649 } 650 651 pub fn title<'txn>(&self, txn: &'txn Transaction, ndb: &Ndb) -> &'txn str { 652 if let TimelineKind::Profile(pubkey) = self.kind { 653 let profile = ndb.get_profile_by_pubkey(txn, pubkey); 654 let m_name = profile 655 .as_ref() 656 .ok() 657 .map(|p| notedeck::name::get_display_name(Some(p)).name()); 658 659 m_name.unwrap_or("Profile") 660 } else { 661 "Unknown" 662 } 663 } 664 } 665 666 /// This saves us from having to construct a transaction if we don't need to 667 /// for a particular column when rendering the title 668 #[derive(Debug)] 669 pub enum ColumnTitle<'a> { 670 Simple(Cow<'static, str>), 671 NeedsDb(TitleNeedsDb<'a>), 672 } 673 674 impl<'a> ColumnTitle<'a> { 675 pub fn simple(title: &'static str) -> Self { 676 Self::Simple(Cow::Borrowed(title)) 677 } 678 679 pub fn formatted(title: String) -> Self { 680 Self::Simple(Cow::Owned(title)) 681 } 682 683 pub fn needs_db(kind: &'a TimelineKind) -> ColumnTitle<'a> { 684 Self::NeedsDb(TitleNeedsDb::new(kind)) 685 } 686 } 687 688 fn contact_filter_state(txn: &Transaction, ndb: &Ndb, pk: &Pubkey) -> FilterState { 689 let contact_filter = contacts_filter(pk); 690 691 let results = ndb 692 .query(txn, std::slice::from_ref(&contact_filter), 1) 693 .expect("contact query failed?"); 694 695 if results.is_empty() { 696 FilterState::needs_remote() 697 } else { 698 let with_hashtags = false; 699 match hybrid_contacts_filter(&results[0].note, Some(pk.bytes()), with_hashtags) { 700 Err(notedeck::Error::Filter(FilterError::EmptyContactList)) => { 701 FilterState::needs_remote() 702 } 703 Err(err) => { 704 error!("Error getting contact filter state: {err}"); 705 FilterState::Broken(FilterError::EmptyContactList) 706 } 707 Ok(filter) => FilterState::ready_hybrid(filter), 708 } 709 } 710 } 711 712 fn last_per_pubkey_filter_state(ndb: &Ndb, pk: &Pubkey) -> FilterState { 713 let contact_filter = contacts_filter(pk.bytes()); 714 715 let txn = Transaction::new(ndb).expect("txn"); 716 let results = ndb 717 .query(&txn, std::slice::from_ref(&contact_filter), 1) 718 .expect("contact query failed?"); 719 720 if results.is_empty() { 721 FilterState::needs_remote() 722 } else { 723 let kind = 1; 724 let notes_per_pk = 1; 725 match filter::last_n_per_pubkey_from_tags(&results[0].note, kind, notes_per_pk) { 726 Err(notedeck::Error::Filter(FilterError::EmptyContactList)) => { 727 FilterState::needs_remote() 728 } 729 Err(err) => { 730 error!("Error getting contact filter state: {err}"); 731 FilterState::Broken(FilterError::EmptyContactList) 732 } 733 Ok(filter) => FilterState::ready(filter), 734 } 735 } 736 } 737 738 fn profile_filter(pk: &[u8; 32]) -> HybridFilter { 739 let local = vec![ 740 NdbQueryPackage { 741 filters: vec![Filter::new() 742 .authors([pk]) 743 .kinds([1]) 744 .limit(default_limit()) 745 .build()], 746 kind: ValidKind::One, 747 }, 748 NdbQueryPackage { 749 filters: vec![Filter::new() 750 .authors([pk]) 751 .kinds([6]) 752 .limit(default_limit()) 753 .build()], 754 kind: ValidKind::Six, 755 }, 756 ]; 757 758 let remote = vec![Filter::new() 759 .authors([pk]) 760 .kinds([1, 6, 0, 3]) 761 .limit(default_remote_limit()) 762 .build()]; 763 764 HybridFilter::split(local, remote) 765 } 766 767 fn search_filter(s: &SearchQuery) -> Vec<Filter> { 768 vec![s.filter().limit(default_limit()).build()] 769 } 770 771 fn universe_filter() -> Vec<Filter> { 772 vec![Filter::new().kinds([1]).limit(default_limit()).build()] 773 }