decks.rs (13947B)
1 use std::collections::{hash_map::ValuesMut, HashMap}; 2 3 use enostr::{Pubkey, RelayPool}; 4 use nostrdb::Transaction; 5 use notedeck::{tr, AppContext, Localization, FALLBACK_PUBKEY}; 6 use tracing::{error, info}; 7 8 use crate::{ 9 column::{Column, Columns}, 10 timeline::{TimelineCache, TimelineKind}, 11 ui::configure_deck::ConfigureDeckResponse, 12 }; 13 14 pub enum DecksAction { 15 Switch(usize), 16 Removing(usize), 17 } 18 19 pub struct DecksCache { 20 account_to_decks: HashMap<Pubkey, Decks>, 21 fallback_pubkey: Pubkey, 22 } 23 24 impl DecksCache { 25 pub fn default_decks_cache(i18n: &mut Localization) -> Self { 26 let mut account_to_decks: HashMap<Pubkey, Decks> = Default::default(); 27 account_to_decks.insert(FALLBACK_PUBKEY(), Decks::default_decks(i18n)); 28 DecksCache::new(account_to_decks, i18n) 29 } 30 31 /// Gets the first column in the currently active user's active deck 32 pub fn selected_column_mut( 33 &mut self, 34 i18n: &mut Localization, 35 accounts: ¬edeck::Accounts, 36 ) -> Option<&mut Column> { 37 self.active_columns_mut(i18n, accounts) 38 .and_then(|ad| ad.selected_mut()) 39 } 40 41 pub fn selected_column(&self, accounts: ¬edeck::Accounts) -> Option<&Column> { 42 self.active_columns(accounts).and_then(|ad| ad.selected()) 43 } 44 45 pub fn selected_column_index(&self, accounts: ¬edeck::Accounts) -> Option<usize> { 46 self.active_columns(accounts).map(|ad| ad.selected as usize) 47 } 48 49 /// Gets a mutable reference to the active columns 50 pub fn active_columns_mut( 51 &mut self, 52 i18n: &mut Localization, 53 accounts: ¬edeck::Accounts, 54 ) -> Option<&mut Columns> { 55 let account = accounts.get_selected_account(); 56 57 self.decks_mut(i18n, &account.key.pubkey) 58 .active_deck_mut() 59 .map(|ad| ad.columns_mut()) 60 } 61 62 /// Gets an immutable reference to the active columns 63 pub fn active_columns(&self, accounts: ¬edeck::Accounts) -> Option<&Columns> { 64 let account = accounts.get_selected_account(); 65 66 self.decks(&account.key.pubkey) 67 .active_deck() 68 .map(|ad| ad.columns()) 69 } 70 71 pub fn new(mut account_to_decks: HashMap<Pubkey, Decks>, i18n: &mut Localization) -> Self { 72 let fallback_pubkey = FALLBACK_PUBKEY(); 73 account_to_decks 74 .entry(fallback_pubkey) 75 .or_insert_with(|| Decks::default_decks(i18n)); 76 77 Self { 78 account_to_decks, 79 fallback_pubkey, 80 } 81 } 82 83 pub fn new_with_demo_config(timeline_cache: &mut TimelineCache, ctx: &mut AppContext) -> Self { 84 let mut account_to_decks: HashMap<Pubkey, Decks> = Default::default(); 85 let fallback_pubkey = FALLBACK_PUBKEY(); 86 account_to_decks.insert( 87 fallback_pubkey, 88 demo_decks(fallback_pubkey, timeline_cache, ctx), 89 ); 90 DecksCache::new(account_to_decks, ctx.i18n) 91 } 92 93 pub fn decks(&self, key: &Pubkey) -> &Decks { 94 self.account_to_decks 95 .get(key) 96 .unwrap_or_else(|| self.fallback()) 97 } 98 99 pub fn decks_mut(&mut self, i18n: &mut Localization, key: &Pubkey) -> &mut Decks { 100 self.account_to_decks 101 .entry(*key) 102 .or_insert_with(|| Decks::default_decks(i18n)) 103 } 104 105 pub fn fallback(&self) -> &Decks { 106 self.account_to_decks 107 .get(&self.fallback_pubkey) 108 .unwrap_or_else(|| panic!("fallback deck not found")) 109 } 110 111 pub fn fallback_mut(&mut self) -> &mut Decks { 112 self.account_to_decks 113 .get_mut(&self.fallback_pubkey) 114 .unwrap_or_else(|| panic!("fallback deck not found")) 115 } 116 117 pub fn add_deck_default( 118 &mut self, 119 ctx: &mut AppContext, 120 timeline_cache: &mut TimelineCache, 121 pubkey: Pubkey, 122 ) { 123 let mut decks = Decks::default_decks(ctx.i18n); 124 125 // add home and notifications for new accounts 126 add_demo_columns( 127 ctx, 128 timeline_cache, 129 pubkey, 130 &mut decks.decks_mut()[0].columns, 131 ); 132 133 self.account_to_decks.insert(pubkey, decks); 134 info!( 135 "Adding new default deck for {:?}. New decks size is {}", 136 pubkey, 137 self.account_to_decks.get(&pubkey).unwrap().decks.len() 138 ); 139 } 140 141 pub fn add_decks(&mut self, key: Pubkey, decks: Decks) { 142 self.account_to_decks.insert(key, decks); 143 info!( 144 "Adding new deck for {:?}. New decks size is {}", 145 key, 146 self.account_to_decks.get(&key).unwrap().decks.len() 147 ); 148 } 149 150 pub fn add_deck(&mut self, key: Pubkey, deck: Deck) { 151 match self.account_to_decks.entry(key) { 152 std::collections::hash_map::Entry::Occupied(mut entry) => { 153 let decks = entry.get_mut(); 154 decks.add_deck(deck); 155 info!( 156 "Created new deck for {:?}. New number of decks is {}", 157 key, 158 decks.decks.len() 159 ); 160 } 161 std::collections::hash_map::Entry::Vacant(entry) => { 162 info!("Created first deck for {:?}", key); 163 entry.insert(Decks::new(deck)); 164 } 165 } 166 } 167 168 pub fn remove( 169 &mut self, 170 i18n: &mut Localization, 171 key: &Pubkey, 172 timeline_cache: &mut TimelineCache, 173 ndb: &mut nostrdb::Ndb, 174 pool: &mut RelayPool, 175 ) { 176 let Some(decks) = self.account_to_decks.remove(key) else { 177 return; 178 }; 179 info!("Removing decks for {:?}", key); 180 181 decks.unsubscribe_all(timeline_cache, ndb, pool); 182 183 if !self.account_to_decks.contains_key(&self.fallback_pubkey) { 184 self.account_to_decks 185 .insert(self.fallback_pubkey, Decks::default_decks(i18n)); 186 } 187 } 188 189 pub fn get_fallback_pubkey(&self) -> &Pubkey { 190 &self.fallback_pubkey 191 } 192 193 pub fn get_all_decks_mut(&mut self) -> ValuesMut<Pubkey, Decks> { 194 self.account_to_decks.values_mut() 195 } 196 197 pub fn get_mapping(&self) -> &HashMap<Pubkey, Decks> { 198 &self.account_to_decks 199 } 200 } 201 202 pub struct Decks { 203 active_deck: usize, 204 removal_request: Option<usize>, 205 decks: Vec<Deck>, 206 } 207 208 impl Decks { 209 pub fn default_decks(i18n: &mut Localization) -> Self { 210 Decks::new(Deck::default_deck(i18n)) 211 } 212 213 pub fn new(deck: Deck) -> Self { 214 let decks = vec![deck]; 215 216 Decks { 217 active_deck: 0, 218 removal_request: None, 219 decks, 220 } 221 } 222 223 pub fn from_decks(active_deck: usize, decks: Vec<Deck>) -> Self { 224 Self { 225 active_deck, 226 removal_request: None, 227 decks, 228 } 229 } 230 231 pub fn active(&self) -> &Deck { 232 self.decks 233 .get(self.active_deck) 234 .expect("active_deck index was invalid") 235 } 236 237 pub fn active_mut(&mut self) -> &mut Deck { 238 self.decks 239 .get_mut(self.active_deck) 240 .expect("active_deck index was invalid") 241 } 242 243 pub fn decks(&self) -> &Vec<Deck> { 244 &self.decks 245 } 246 247 fn active_deck_index(&self) -> Option<usize> { 248 if self.decks.is_empty() { 249 return None; 250 } 251 252 let active = self.active_index(); 253 if active > (self.decks.len() - 1) { 254 return None; 255 } 256 257 Some(active) 258 } 259 260 pub fn active_deck(&self) -> Option<&Deck> { 261 self.active_deck_index().map(|ind| &self.decks[ind]) 262 } 263 264 pub fn active_deck_mut(&mut self) -> Option<&mut Deck> { 265 self.active_deck_index().map(|ind| &mut self.decks[ind]) 266 } 267 268 pub fn decks_mut(&mut self) -> &mut Vec<Deck> { 269 &mut self.decks 270 } 271 272 pub fn add_deck(&mut self, deck: Deck) { 273 self.decks.push(deck); 274 } 275 276 pub fn active_index(&self) -> usize { 277 self.active_deck 278 } 279 280 pub fn set_active(&mut self, index: usize) { 281 if index < self.decks.len() { 282 self.active_deck = index; 283 } else { 284 error!( 285 "requested deck change that is invalid. decks len: {}, requested index: {}", 286 self.decks.len(), 287 index 288 ); 289 } 290 } 291 292 pub fn remove_deck( 293 &mut self, 294 index: usize, 295 timeline_cache: &mut TimelineCache, 296 ndb: &mut nostrdb::Ndb, 297 pool: &mut enostr::RelayPool, 298 ) { 299 let Some(deck) = self.remove_deck_internal(index) else { 300 return; 301 }; 302 303 delete_deck(deck, timeline_cache, ndb, pool); 304 } 305 306 fn remove_deck_internal(&mut self, index: usize) -> Option<Deck> { 307 let mut res = None; 308 if index < self.decks.len() { 309 if self.decks.len() > 1 { 310 res = Some(self.decks.remove(index)); 311 312 let info_prefix = format!("Removed deck at index {index}"); 313 match index.cmp(&self.active_deck) { 314 std::cmp::Ordering::Less => { 315 info!( 316 "{}. The active deck was index {}, now it is {}", 317 info_prefix, 318 self.active_deck, 319 self.active_deck - 1 320 ); 321 self.active_deck -= 1 322 } 323 std::cmp::Ordering::Greater => { 324 info!( 325 "{}. Active deck remains at index {}.", 326 info_prefix, self.active_deck 327 ) 328 } 329 std::cmp::Ordering::Equal => { 330 if index != 0 { 331 info!( 332 "{}. Active deck was index {}, now it is {}", 333 info_prefix, 334 self.active_deck, 335 self.active_deck - 1 336 ); 337 self.active_deck -= 1; 338 } else { 339 info!( 340 "{}. Active deck remains at index {}.", 341 info_prefix, self.active_deck 342 ) 343 } 344 } 345 } 346 self.removal_request = None; 347 } else { 348 error!("attempted unsucessfully to remove the last deck for this account"); 349 } 350 } else { 351 error!("index was out of bounds"); 352 } 353 res 354 } 355 356 pub fn unsubscribe_all( 357 self, 358 timeline_cache: &mut TimelineCache, 359 ndb: &mut nostrdb::Ndb, 360 pool: &mut enostr::RelayPool, 361 ) { 362 for deck in self.decks { 363 delete_deck(deck, timeline_cache, ndb, pool); 364 } 365 } 366 } 367 368 fn delete_deck( 369 mut deck: Deck, 370 timeline_cache: &mut TimelineCache, 371 ndb: &mut nostrdb::Ndb, 372 pool: &mut enostr::RelayPool, 373 ) { 374 let cols = deck.columns_mut(); 375 let num_cols = cols.num_columns(); 376 for i in (0..num_cols).rev() { 377 let kinds_to_pop = cols.delete_column(i); 378 379 for kind in &kinds_to_pop { 380 if let Err(err) = timeline_cache.pop(kind, ndb, pool) { 381 error!("error popping timeline: {err}"); 382 } 383 } 384 } 385 } 386 387 pub struct Deck { 388 pub icon: char, 389 pub name: String, 390 columns: Columns, 391 } 392 393 impl Deck { 394 pub fn default_icon() -> char { 395 '🇩' 396 } 397 398 fn default_deck(i18n: &mut Localization) -> Self { 399 let columns = Columns::default(); 400 Self { 401 columns, 402 icon: Deck::default_icon(), 403 name: Deck::default_name(i18n).to_string(), 404 } 405 } 406 407 pub fn default_name(i18n: &mut Localization) -> String { 408 tr!(i18n, "Default Deck", "Name of the default deck feed") 409 } 410 411 pub fn new(icon: char, name: String) -> Self { 412 let mut columns = Columns::default(); 413 414 columns.new_column_picker(); 415 416 Self { 417 icon, 418 name, 419 columns, 420 } 421 } 422 423 pub fn new_with_columns(icon: char, name: String, columns: Columns) -> Self { 424 Self { 425 icon, 426 name, 427 columns, 428 } 429 } 430 431 pub fn columns(&self) -> &Columns { 432 &self.columns 433 } 434 435 pub fn columns_mut(&mut self) -> &mut Columns { 436 &mut self.columns 437 } 438 439 pub fn edit(&mut self, changes: ConfigureDeckResponse) { 440 self.name = changes.name; 441 self.icon = changes.icon; 442 } 443 } 444 445 pub fn add_demo_columns( 446 ctx: &mut AppContext, 447 timeline_cache: &mut TimelineCache, 448 pubkey: Pubkey, 449 columns: &mut Columns, 450 ) { 451 let timeline_kinds = [ 452 TimelineKind::contact_list(pubkey), 453 TimelineKind::notifications(pubkey), 454 ]; 455 456 let txn = Transaction::new(ctx.ndb).unwrap(); 457 458 for kind in &timeline_kinds { 459 if let Some(results) = columns.add_new_timeline_column( 460 timeline_cache, 461 &txn, 462 ctx.ndb, 463 ctx.note_cache, 464 ctx.pool, 465 kind, 466 ) { 467 results.process( 468 ctx.ndb, 469 ctx.note_cache, 470 &txn, 471 timeline_cache, 472 ctx.unknown_ids, 473 ); 474 } 475 } 476 } 477 478 pub fn demo_decks( 479 demo_pubkey: Pubkey, 480 timeline_cache: &mut TimelineCache, 481 ctx: &mut AppContext, 482 ) -> Decks { 483 let deck = { 484 let mut columns = Columns::default(); 485 486 add_demo_columns(ctx, timeline_cache, demo_pubkey, &mut columns); 487 488 //columns.add_new_timeline_column(Timeline::hashtag("introductions".to_string())); 489 490 Deck { 491 icon: Deck::default_icon(), 492 name: Deck::default_name(ctx.i18n).to_string(), 493 columns, 494 } 495 }; 496 497 Decks::new(deck) 498 }