notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

kind.rs (31388B)


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