notedeck

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

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: &notedeck::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: &notedeck::Accounts) -> Option<&Column> {
     42         self.active_columns(accounts).and_then(|ad| ad.selected())
     43     }
     44 
     45     pub fn selected_column_index(&self, accounts: &notedeck::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: &notedeck::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: &notedeck::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 }