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 }