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