notedeck

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

kind.rs (9329B)


      1 use crate::error::Error;
      2 use crate::timeline::{Timeline, TimelineTab};
      3 use enostr::{Filter, Pubkey};
      4 use nostrdb::{Ndb, Transaction};
      5 use notedeck::{filter::default_limit, FilterError, FilterState, RootNoteIdBuf};
      6 use serde::{Deserialize, Serialize};
      7 use std::{borrow::Cow, fmt::Display};
      8 use tracing::{error, warn};
      9 
     10 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
     11 pub enum PubkeySource {
     12     Explicit(Pubkey),
     13     DeckAuthor,
     14 }
     15 
     16 #[derive(Debug, Clone, PartialEq, Eq)]
     17 pub enum ListKind {
     18     Contact(PubkeySource),
     19 }
     20 
     21 impl PubkeySource {
     22     pub fn to_pubkey<'a>(&'a self, deck_author: &'a Pubkey) -> &'a Pubkey {
     23         match self {
     24             PubkeySource::Explicit(pk) => pk,
     25             PubkeySource::DeckAuthor => deck_author,
     26         }
     27     }
     28 
     29     pub fn to_pubkey_bytes<'a>(&'a self, deck_author: &'a [u8; 32]) -> &'a [u8; 32] {
     30         match self {
     31             PubkeySource::Explicit(pk) => pk.bytes(),
     32             PubkeySource::DeckAuthor => deck_author,
     33         }
     34     }
     35 }
     36 
     37 impl ListKind {
     38     pub fn pubkey_source(&self) -> Option<&PubkeySource> {
     39         match self {
     40             ListKind::Contact(pk_src) => Some(pk_src),
     41         }
     42     }
     43 }
     44 
     45 ///
     46 /// What kind of timeline is it?
     47 ///   - Follow List
     48 ///   - Notifications
     49 ///   - DM
     50 ///   - filter
     51 ///   - ... etc
     52 ///
     53 #[derive(Debug, Clone, PartialEq, Eq)]
     54 pub enum TimelineKind {
     55     List(ListKind),
     56 
     57     Notifications(PubkeySource),
     58 
     59     Profile(PubkeySource),
     60 
     61     /// This could be any note id, doesn't need to be the root id
     62     Thread(RootNoteIdBuf),
     63 
     64     Universe,
     65 
     66     /// Generic filter
     67     Generic,
     68 
     69     Hashtag(String),
     70 }
     71 
     72 impl Display for TimelineKind {
     73     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
     74         match self {
     75             TimelineKind::List(ListKind::Contact(_src)) => f.write_str("Contacts"),
     76             TimelineKind::Generic => f.write_str("Timeline"),
     77             TimelineKind::Notifications(_) => f.write_str("Notifications"),
     78             TimelineKind::Profile(_) => f.write_str("Profile"),
     79             TimelineKind::Universe => f.write_str("Universe"),
     80             TimelineKind::Hashtag(_) => f.write_str("Hashtag"),
     81             TimelineKind::Thread(_) => f.write_str("Thread"),
     82         }
     83     }
     84 }
     85 
     86 impl TimelineKind {
     87     pub fn pubkey_source(&self) -> Option<&PubkeySource> {
     88         match self {
     89             TimelineKind::List(list_kind) => list_kind.pubkey_source(),
     90             TimelineKind::Notifications(pk_src) => Some(pk_src),
     91             TimelineKind::Profile(pk_src) => Some(pk_src),
     92             TimelineKind::Universe => None,
     93             TimelineKind::Generic => None,
     94             TimelineKind::Hashtag(_ht) => None,
     95             TimelineKind::Thread(_ht) => None,
     96         }
     97     }
     98 
     99     pub fn contact_list(pk: PubkeySource) -> Self {
    100         TimelineKind::List(ListKind::Contact(pk))
    101     }
    102 
    103     pub fn is_contacts(&self) -> bool {
    104         matches!(self, TimelineKind::List(ListKind::Contact(_)))
    105     }
    106 
    107     pub fn profile(pk: PubkeySource) -> Self {
    108         TimelineKind::Profile(pk)
    109     }
    110 
    111     pub fn thread(root_id: RootNoteIdBuf) -> Self {
    112         TimelineKind::Thread(root_id)
    113     }
    114 
    115     pub fn is_notifications(&self) -> bool {
    116         matches!(self, TimelineKind::Notifications(_))
    117     }
    118 
    119     pub fn notifications(pk: PubkeySource) -> Self {
    120         TimelineKind::Notifications(pk)
    121     }
    122 
    123     pub fn into_timeline(self, ndb: &Ndb, default_user: Option<&[u8; 32]>) -> Option<Timeline> {
    124         match self {
    125             TimelineKind::Universe => Some(Timeline::new(
    126                 TimelineKind::Universe,
    127                 FilterState::ready(vec![Filter::new()
    128                     .kinds([1])
    129                     .limit(default_limit())
    130                     .build()]),
    131                 TimelineTab::no_replies(),
    132             )),
    133 
    134             TimelineKind::Thread(root_id) => Some(Timeline::thread(root_id)),
    135 
    136             TimelineKind::Generic => {
    137                 warn!("you can't convert a TimelineKind::Generic to a Timeline");
    138                 None
    139             }
    140 
    141             TimelineKind::Profile(pk_src) => {
    142                 let pk = match &pk_src {
    143                     PubkeySource::DeckAuthor => default_user?,
    144                     PubkeySource::Explicit(pk) => pk.bytes(),
    145                 };
    146 
    147                 let filter = Filter::new()
    148                     .authors([pk])
    149                     .kinds([1])
    150                     .limit(default_limit())
    151                     .build();
    152 
    153                 Some(Timeline::new(
    154                     TimelineKind::profile(pk_src),
    155                     FilterState::ready(vec![filter]),
    156                     TimelineTab::full_tabs(),
    157                 ))
    158             }
    159 
    160             TimelineKind::Notifications(pk_src) => {
    161                 let pk = match &pk_src {
    162                     PubkeySource::DeckAuthor => default_user?,
    163                     PubkeySource::Explicit(pk) => pk.bytes(),
    164                 };
    165 
    166                 let notifications_filter = Filter::new()
    167                     .pubkeys([pk])
    168                     .kinds([1])
    169                     .limit(default_limit())
    170                     .build();
    171 
    172                 Some(Timeline::new(
    173                     TimelineKind::notifications(pk_src),
    174                     FilterState::ready(vec![notifications_filter]),
    175                     TimelineTab::only_notes_and_replies(),
    176                 ))
    177             }
    178 
    179             TimelineKind::Hashtag(hashtag) => Some(Timeline::hashtag(hashtag)),
    180 
    181             TimelineKind::List(ListKind::Contact(pk_src)) => {
    182                 let pk = match &pk_src {
    183                     PubkeySource::DeckAuthor => default_user?,
    184                     PubkeySource::Explicit(pk) => pk.bytes(),
    185                 };
    186 
    187                 let contact_filter = Filter::new().authors([pk]).kinds([3]).limit(1).build();
    188 
    189                 let txn = Transaction::new(ndb).expect("txn");
    190                 let results = ndb
    191                     .query(&txn, &[contact_filter.clone()], 1)
    192                     .expect("contact query failed?");
    193 
    194                 if results.is_empty() {
    195                     return Some(Timeline::new(
    196                         TimelineKind::contact_list(pk_src),
    197                         FilterState::needs_remote(vec![contact_filter.clone()]),
    198                         TimelineTab::full_tabs(),
    199                     ));
    200                 }
    201 
    202                 match Timeline::contact_list(&results[0].note, pk_src.clone(), default_user) {
    203                     Err(Error::App(notedeck::Error::Filter(FilterError::EmptyContactList))) => {
    204                         Some(Timeline::new(
    205                             TimelineKind::contact_list(pk_src),
    206                             FilterState::needs_remote(vec![contact_filter]),
    207                             TimelineTab::full_tabs(),
    208                         ))
    209                     }
    210                     Err(e) => {
    211                         error!("Unexpected error: {e}");
    212                         None
    213                     }
    214                     Ok(tl) => Some(tl),
    215                 }
    216             }
    217         }
    218     }
    219 
    220     pub fn to_title(&self) -> ColumnTitle<'_> {
    221         match self {
    222             TimelineKind::List(list_kind) => match list_kind {
    223                 ListKind::Contact(_pubkey_source) => ColumnTitle::simple("Contacts"),
    224             },
    225             TimelineKind::Notifications(_pubkey_source) => ColumnTitle::simple("Notifications"),
    226             TimelineKind::Profile(_pubkey_source) => ColumnTitle::needs_db(self),
    227             TimelineKind::Thread(_root_id) => ColumnTitle::simple("Thread"),
    228             TimelineKind::Universe => ColumnTitle::simple("Universe"),
    229             TimelineKind::Generic => ColumnTitle::simple("Custom"),
    230             TimelineKind::Hashtag(hashtag) => ColumnTitle::formatted(hashtag.to_string()),
    231         }
    232     }
    233 }
    234 
    235 #[derive(Debug)]
    236 pub struct TitleNeedsDb<'a> {
    237     kind: &'a TimelineKind,
    238 }
    239 
    240 impl<'a> TitleNeedsDb<'a> {
    241     pub fn new(kind: &'a TimelineKind) -> Self {
    242         TitleNeedsDb { kind }
    243     }
    244 
    245     pub fn title<'txn>(
    246         &self,
    247         txn: &'txn Transaction,
    248         ndb: &Ndb,
    249         deck_author: Option<&Pubkey>,
    250     ) -> &'txn str {
    251         if let TimelineKind::Profile(pubkey_source) = self.kind {
    252             if let Some(deck_author) = deck_author {
    253                 let pubkey = pubkey_source.to_pubkey(deck_author);
    254                 let profile = ndb.get_profile_by_pubkey(txn, pubkey);
    255                 let m_name = profile
    256                     .as_ref()
    257                     .ok()
    258                     .map(|p| crate::profile::get_display_name(Some(p)).name());
    259 
    260                 m_name.unwrap_or("Profile")
    261             } else {
    262                 // why would be there be no deck author? weird
    263                 "nostrich"
    264             }
    265         } else {
    266             "Unknown"
    267         }
    268     }
    269 }
    270 
    271 /// This saves us from having to construct a transaction if we don't need to
    272 /// for a particular column when rendering the title
    273 #[derive(Debug)]
    274 pub enum ColumnTitle<'a> {
    275     Simple(Cow<'static, str>),
    276     NeedsDb(TitleNeedsDb<'a>),
    277 }
    278 
    279 impl<'a> ColumnTitle<'a> {
    280     pub fn simple(title: &'static str) -> Self {
    281         Self::Simple(Cow::Borrowed(title))
    282     }
    283 
    284     pub fn formatted(title: String) -> Self {
    285         Self::Simple(Cow::Owned(title))
    286     }
    287 
    288     pub fn needs_db(kind: &'a TimelineKind) -> ColumnTitle<'a> {
    289         Self::NeedsDb(TitleNeedsDb::new(kind))
    290     }
    291 }