notedeck

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

decks.rs (28468B)


      1 use std::{collections::HashMap, fmt, str::FromStr};
      2 
      3 use enostr::{NoteId, Pubkey};
      4 use nostrdb::Ndb;
      5 use serde::{Deserialize, Serialize};
      6 use tracing::{error, info};
      7 
      8 use crate::{
      9     accounts::AccountsRoute,
     10     column::{Columns, IntermediaryRoute},
     11     decks::{Deck, Decks, DecksCache},
     12     route::Route,
     13     timeline::{kind::ListKind, PubkeySource, TimelineKind, TimelineRoute},
     14     ui::add_column::AddColumnRoute,
     15     Error,
     16 };
     17 
     18 use notedeck::{storage, DataPath, DataPathType, Directory};
     19 
     20 pub static DECKS_CACHE_FILE: &str = "decks_cache.json";
     21 
     22 pub fn load_decks_cache(path: &DataPath, ndb: &Ndb) -> Option<DecksCache> {
     23     let data_path = path.path(DataPathType::Setting);
     24 
     25     let decks_cache_str = match Directory::new(data_path).get_file(DECKS_CACHE_FILE.to_owned()) {
     26         Ok(s) => s,
     27         Err(e) => {
     28             error!(
     29                 "Could not read decks cache from file {}:  {}",
     30                 DECKS_CACHE_FILE, e
     31             );
     32             return None;
     33         }
     34     };
     35 
     36     let serializable_decks_cache =
     37         serde_json::from_str::<SerializableDecksCache>(&decks_cache_str).ok()?;
     38 
     39     serializable_decks_cache.decks_cache(ndb).ok()
     40 }
     41 
     42 pub fn save_decks_cache(path: &DataPath, decks_cache: &DecksCache) {
     43     let serialized_decks_cache =
     44         match serde_json::to_string(&SerializableDecksCache::to_serializable(decks_cache)) {
     45             Ok(s) => s,
     46             Err(e) => {
     47                 error!("Could not serialize decks cache: {}", e);
     48                 return;
     49             }
     50         };
     51 
     52     let data_path = path.path(DataPathType::Setting);
     53 
     54     if let Err(e) = storage::write_file(
     55         &data_path,
     56         DECKS_CACHE_FILE.to_string(),
     57         &serialized_decks_cache,
     58     ) {
     59         error!(
     60             "Could not write decks cache to file {}: {}",
     61             DECKS_CACHE_FILE, e
     62         );
     63     } else {
     64         info!("Successfully wrote decks cache to {}", DECKS_CACHE_FILE);
     65     }
     66 }
     67 
     68 #[derive(Serialize, Deserialize)]
     69 struct SerializableDecksCache {
     70     #[serde(serialize_with = "serialize_map", deserialize_with = "deserialize_map")]
     71     decks_cache: HashMap<Pubkey, SerializableDecks>,
     72 }
     73 
     74 impl SerializableDecksCache {
     75     fn to_serializable(decks_cache: &DecksCache) -> Self {
     76         SerializableDecksCache {
     77             decks_cache: decks_cache
     78                 .get_mapping()
     79                 .iter()
     80                 .map(|(k, v)| (*k, SerializableDecks::from_decks(v)))
     81                 .collect(),
     82         }
     83     }
     84 
     85     pub fn decks_cache(self, ndb: &Ndb) -> Result<DecksCache, Error> {
     86         let account_to_decks = self
     87             .decks_cache
     88             .into_iter()
     89             .map(|(pubkey, serializable_decks)| {
     90                 let deck_key = pubkey.bytes();
     91                 serializable_decks
     92                     .decks(ndb, deck_key)
     93                     .map(|decks| (pubkey, decks))
     94             })
     95             .collect::<Result<HashMap<Pubkey, Decks>, Error>>()?;
     96 
     97         Ok(DecksCache::new(account_to_decks))
     98     }
     99 }
    100 
    101 fn serialize_map<S>(
    102     map: &HashMap<Pubkey, SerializableDecks>,
    103     serializer: S,
    104 ) -> Result<S::Ok, S::Error>
    105 where
    106     S: serde::Serializer,
    107 {
    108     let stringified_map: HashMap<String, &SerializableDecks> =
    109         map.iter().map(|(k, v)| (k.hex(), v)).collect();
    110     stringified_map.serialize(serializer)
    111 }
    112 
    113 fn deserialize_map<'de, D>(deserializer: D) -> Result<HashMap<Pubkey, SerializableDecks>, D::Error>
    114 where
    115     D: serde::Deserializer<'de>,
    116 {
    117     let stringified_map: HashMap<String, SerializableDecks> = HashMap::deserialize(deserializer)?;
    118 
    119     stringified_map
    120         .into_iter()
    121         .map(|(k, v)| {
    122             let key = Pubkey::from_hex(&k).map_err(serde::de::Error::custom)?;
    123             Ok((key, v))
    124         })
    125         .collect()
    126 }
    127 
    128 #[derive(Serialize, Deserialize)]
    129 struct SerializableDecks {
    130     active_deck: usize,
    131     decks: Vec<SerializableDeck>,
    132 }
    133 
    134 impl SerializableDecks {
    135     pub fn from_decks(decks: &Decks) -> Self {
    136         Self {
    137             active_deck: decks.active_index(),
    138             decks: decks
    139                 .decks()
    140                 .iter()
    141                 .map(SerializableDeck::from_deck)
    142                 .collect(),
    143         }
    144     }
    145 
    146     fn decks(self, ndb: &Ndb, deck_key: &[u8; 32]) -> Result<Decks, Error> {
    147         Ok(Decks::from_decks(
    148             self.active_deck,
    149             self.decks
    150                 .into_iter()
    151                 .map(|d| d.deck(ndb, deck_key))
    152                 .collect::<Result<_, _>>()?,
    153         ))
    154     }
    155 }
    156 
    157 #[derive(Serialize, Deserialize)]
    158 struct SerializableDeck {
    159     metadata: Vec<String>,
    160     columns: Vec<Vec<String>>,
    161 }
    162 
    163 #[derive(PartialEq, Clone)]
    164 enum MetadataKeyword {
    165     Icon,
    166     Name,
    167 }
    168 
    169 impl MetadataKeyword {
    170     const MAPPING: &'static [(&'static str, MetadataKeyword)] = &[
    171         ("icon", MetadataKeyword::Icon),
    172         ("name", MetadataKeyword::Name),
    173     ];
    174 }
    175 impl fmt::Display for MetadataKeyword {
    176     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    177         if let Some(name) = MetadataKeyword::MAPPING
    178             .iter()
    179             .find(|(_, keyword)| keyword == self)
    180             .map(|(name, _)| *name)
    181         {
    182             write!(f, "{}", name)
    183         } else {
    184             write!(f, "UnknownMetadataKeyword")
    185         }
    186     }
    187 }
    188 
    189 impl FromStr for MetadataKeyword {
    190     type Err = Error;
    191 
    192     fn from_str(serialized: &str) -> Result<Self, Self::Err> {
    193         MetadataKeyword::MAPPING
    194             .iter()
    195             .find(|(name, _)| *name == serialized)
    196             .map(|(_, keyword)| keyword.clone())
    197             .ok_or(Error::Generic(
    198                 "Could not convert string to Keyword enum".to_owned(),
    199             ))
    200     }
    201 }
    202 
    203 struct MetadataPayload {
    204     keyword: MetadataKeyword,
    205     value: String,
    206 }
    207 
    208 impl MetadataPayload {
    209     fn new(keyword: MetadataKeyword, value: String) -> Self {
    210         Self { keyword, value }
    211     }
    212 }
    213 
    214 fn serialize_metadata(payloads: Vec<MetadataPayload>) -> Vec<String> {
    215     payloads
    216         .into_iter()
    217         .map(|payload| format!("{}:{}", payload.keyword, payload.value))
    218         .collect()
    219 }
    220 
    221 fn deserialize_metadata(serialized_metadatas: Vec<String>) -> Option<Vec<MetadataPayload>> {
    222     let mut payloads = Vec::new();
    223     for serialized_metadata in serialized_metadatas {
    224         let cur_split: Vec<&str> = serialized_metadata.split(':').collect();
    225         if cur_split.len() != 2 {
    226             continue;
    227         }
    228 
    229         if let Ok(keyword) = MetadataKeyword::from_str(cur_split.first().unwrap()) {
    230             payloads.push(MetadataPayload {
    231                 keyword,
    232                 value: cur_split.get(1).unwrap().to_string(),
    233             });
    234         }
    235     }
    236 
    237     if payloads.is_empty() {
    238         None
    239     } else {
    240         Some(payloads)
    241     }
    242 }
    243 
    244 impl SerializableDeck {
    245     pub fn from_deck(deck: &Deck) -> Self {
    246         let columns = serialize_columns(deck.columns());
    247 
    248         let metadata = serialize_metadata(vec![
    249             MetadataPayload::new(MetadataKeyword::Icon, deck.icon.to_string()),
    250             MetadataPayload::new(MetadataKeyword::Name, deck.name.clone()),
    251         ]);
    252 
    253         SerializableDeck { metadata, columns }
    254     }
    255 
    256     pub fn deck(self, ndb: &Ndb, deck_user: &[u8; 32]) -> Result<Deck, Error> {
    257         let columns = deserialize_columns(ndb, deck_user, self.columns);
    258         let deserialized_metadata = deserialize_metadata(self.metadata)
    259             .ok_or(Error::Generic("Could not deserialize metadata".to_owned()))?;
    260 
    261         let icon = deserialized_metadata
    262             .iter()
    263             .find(|p| p.keyword == MetadataKeyword::Icon)
    264             .map_or_else(|| "🇩", |f| &f.value);
    265         let name = deserialized_metadata
    266             .iter()
    267             .find(|p| p.keyword == MetadataKeyword::Name)
    268             .map_or_else(|| "Deck", |f| &f.value)
    269             .to_string();
    270 
    271         Ok(Deck::new_with_columns(
    272             icon.parse::<char>()
    273                 .map_err(|_| Error::Generic("could not convert String -> char".to_owned()))?,
    274             name,
    275             columns,
    276         ))
    277     }
    278 }
    279 
    280 fn serialize_columns(columns: &Columns) -> Vec<Vec<String>> {
    281     let mut cols_serialized: Vec<Vec<String>> = Vec::new();
    282 
    283     for column in columns.columns() {
    284         let mut column_routes = Vec::new();
    285         for route in column.router().routes() {
    286             if let Some(route_str) = serialize_route(route, columns) {
    287                 column_routes.push(route_str);
    288             }
    289         }
    290         cols_serialized.push(column_routes);
    291     }
    292 
    293     cols_serialized
    294 }
    295 
    296 fn deserialize_columns(ndb: &Ndb, deck_user: &[u8; 32], serialized: Vec<Vec<String>>) -> Columns {
    297     let mut cols = Columns::new();
    298     for serialized_routes in serialized {
    299         let mut cur_routes = Vec::new();
    300         for serialized_route in serialized_routes {
    301             let selections = Selection::from_serialized(&serialized_route);
    302             if let Some(route_intermediary) = selections_to_route(selections.clone()) {
    303                 if let Some(ir) = route_intermediary.intermediary_route(ndb, Some(deck_user)) {
    304                     match &ir {
    305                         IntermediaryRoute::Route(Route::Timeline(TimelineRoute::Thread(_)))
    306                         | IntermediaryRoute::Route(Route::Timeline(TimelineRoute::Profile(_))) => {
    307                             // Do nothing. TimelineRoute Threads & Profiles not yet supported for deserialization
    308                         }
    309                         _ => cur_routes.push(ir),
    310                     }
    311                 }
    312             } else {
    313                 error!(
    314                     "could not turn selections to RouteIntermediary: {:?}",
    315                     selections
    316                 );
    317             }
    318         }
    319 
    320         if !cur_routes.is_empty() {
    321             cols.insert_intermediary_routes(cur_routes);
    322         }
    323     }
    324 
    325     cols
    326 }
    327 
    328 #[derive(Clone, Debug)]
    329 enum Selection {
    330     Keyword(Keyword),
    331     Payload(String),
    332 }
    333 
    334 #[derive(Clone, PartialEq, Debug)]
    335 enum Keyword {
    336     Notifs,
    337     Universe,
    338     Contact,
    339     Explicit,
    340     DeckAuthor,
    341     Profile,
    342     Hashtag,
    343     Generic,
    344     Thread,
    345     Reply,
    346     Quote,
    347     Account,
    348     Show,
    349     New,
    350     Relay,
    351     Compose,
    352     Column,
    353     NotificationSelection,
    354     ExternalNotifSelection,
    355     HashtagSelection,
    356     Support,
    357     Deck,
    358     Edit,
    359     IndividualSelection,
    360     ExternalIndividualSelection,
    361 }
    362 
    363 impl Keyword {
    364     const MAPPING: &'static [(&'static str, Keyword, bool)] = &[
    365         ("notifs", Keyword::Notifs, false),
    366         ("universe", Keyword::Universe, false),
    367         ("contact", Keyword::Contact, false),
    368         ("explicit", Keyword::Explicit, true),
    369         ("deck_author", Keyword::DeckAuthor, false),
    370         ("profile", Keyword::Profile, true),
    371         ("hashtag", Keyword::Hashtag, true),
    372         ("generic", Keyword::Generic, false),
    373         ("thread", Keyword::Thread, true),
    374         ("reply", Keyword::Reply, true),
    375         ("quote", Keyword::Quote, true),
    376         ("account", Keyword::Account, false),
    377         ("show", Keyword::Show, false),
    378         ("new", Keyword::New, false),
    379         ("relay", Keyword::Relay, false),
    380         ("compose", Keyword::Compose, false),
    381         ("column", Keyword::Column, false),
    382         (
    383             "notification_selection",
    384             Keyword::NotificationSelection,
    385             false,
    386         ),
    387         (
    388             "external_notif_selection",
    389             Keyword::ExternalNotifSelection,
    390             false,
    391         ),
    392         ("hashtag_selection", Keyword::HashtagSelection, false),
    393         ("support", Keyword::Support, false),
    394         ("deck", Keyword::Deck, false),
    395         ("edit", Keyword::Edit, true),
    396     ];
    397 
    398     fn has_payload(&self) -> bool {
    399         Keyword::MAPPING
    400             .iter()
    401             .find(|(_, keyword, _)| keyword == self)
    402             .map(|(_, _, has_payload)| *has_payload)
    403             .unwrap_or(false)
    404     }
    405 }
    406 
    407 impl fmt::Display for Keyword {
    408     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    409         if let Some(name) = Keyword::MAPPING
    410             .iter()
    411             .find(|(_, keyword, _)| keyword == self)
    412             .map(|(name, _, _)| *name)
    413         {
    414             write!(f, "{}", name)
    415         } else {
    416             write!(f, "UnknownKeyword")
    417         }
    418     }
    419 }
    420 
    421 impl FromStr for Keyword {
    422     type Err = Error;
    423 
    424     fn from_str(serialized: &str) -> Result<Self, Self::Err> {
    425         Keyword::MAPPING
    426             .iter()
    427             .find(|(name, _, _)| *name == serialized)
    428             .map(|(_, keyword, _)| keyword.clone())
    429             .ok_or(Error::Generic(
    430                 "Could not convert string to Keyword enum".to_owned(),
    431             ))
    432     }
    433 }
    434 
    435 enum CleanIntermediaryRoute {
    436     ToTimeline(TimelineKind),
    437     ToRoute(Route),
    438 }
    439 
    440 impl CleanIntermediaryRoute {
    441     fn intermediary_route(self, ndb: &Ndb, user: Option<&[u8; 32]>) -> Option<IntermediaryRoute> {
    442         match self {
    443             CleanIntermediaryRoute::ToTimeline(timeline_kind) => Some(IntermediaryRoute::Timeline(
    444                 timeline_kind.into_timeline(ndb, user)?,
    445             )),
    446             CleanIntermediaryRoute::ToRoute(route) => Some(IntermediaryRoute::Route(route)),
    447         }
    448     }
    449 }
    450 
    451 // TODO: The public-accessible version will be a subset of this
    452 fn serialize_route(route: &Route, columns: &Columns) -> Option<String> {
    453     let mut selections: Vec<Selection> = Vec::new();
    454     match route {
    455         Route::Timeline(timeline_route) => match timeline_route {
    456             TimelineRoute::Timeline(timeline_id) => {
    457                 if let Some(timeline) = columns.find_timeline(*timeline_id) {
    458                     match &timeline.kind {
    459                         TimelineKind::List(list_kind) => match list_kind {
    460                             ListKind::Contact(pubkey_source) => {
    461                                 selections.push(Selection::Keyword(Keyword::Contact));
    462                                 selections.extend(generate_pubkey_selections(pubkey_source));
    463                             }
    464                         },
    465                         TimelineKind::Notifications(pubkey_source) => {
    466                             selections.push(Selection::Keyword(Keyword::Notifs));
    467                             selections.extend(generate_pubkey_selections(pubkey_source));
    468                         }
    469                         TimelineKind::Profile(pubkey_source) => {
    470                             selections.push(Selection::Keyword(Keyword::Profile));
    471                             selections.extend(generate_pubkey_selections(pubkey_source));
    472                         }
    473                         TimelineKind::Universe => {
    474                             selections.push(Selection::Keyword(Keyword::Universe))
    475                         }
    476                         TimelineKind::Generic => {
    477                             selections.push(Selection::Keyword(Keyword::Generic))
    478                         }
    479                         TimelineKind::Hashtag(hashtag) => {
    480                             selections.push(Selection::Keyword(Keyword::Hashtag));
    481                             selections.push(Selection::Payload(hashtag.to_string()));
    482                         }
    483                     }
    484                 }
    485             }
    486             TimelineRoute::Thread(note_id) => {
    487                 selections.push(Selection::Keyword(Keyword::Thread));
    488                 selections.push(Selection::Payload(note_id.hex()));
    489             }
    490             TimelineRoute::Profile(pubkey) => {
    491                 selections.push(Selection::Keyword(Keyword::Profile));
    492                 selections.push(Selection::Keyword(Keyword::Explicit));
    493                 selections.push(Selection::Payload(pubkey.hex()));
    494             }
    495             TimelineRoute::Reply(note_id) => {
    496                 selections.push(Selection::Keyword(Keyword::Reply));
    497                 selections.push(Selection::Payload(note_id.hex()));
    498             }
    499             TimelineRoute::Quote(note_id) => {
    500                 selections.push(Selection::Keyword(Keyword::Quote));
    501                 selections.push(Selection::Payload(note_id.hex()));
    502             }
    503         },
    504         Route::Accounts(accounts_route) => {
    505             selections.push(Selection::Keyword(Keyword::Account));
    506             match accounts_route {
    507                 AccountsRoute::Accounts => selections.push(Selection::Keyword(Keyword::Show)),
    508                 AccountsRoute::AddAccount => selections.push(Selection::Keyword(Keyword::New)),
    509             }
    510         }
    511         Route::Relays => selections.push(Selection::Keyword(Keyword::Relay)),
    512         Route::ComposeNote => selections.push(Selection::Keyword(Keyword::Compose)),
    513         Route::AddColumn(add_column_route) => {
    514             selections.push(Selection::Keyword(Keyword::Column));
    515             match add_column_route {
    516                 AddColumnRoute::Base => (),
    517                 AddColumnRoute::UndecidedNotification => {
    518                     selections.push(Selection::Keyword(Keyword::NotificationSelection))
    519                 }
    520                 AddColumnRoute::ExternalNotification => {
    521                     selections.push(Selection::Keyword(Keyword::ExternalNotifSelection))
    522                 }
    523                 AddColumnRoute::Hashtag => {
    524                     selections.push(Selection::Keyword(Keyword::HashtagSelection))
    525                 }
    526                 AddColumnRoute::UndecidedIndividual => {
    527                     selections.push(Selection::Keyword(Keyword::IndividualSelection))
    528                 }
    529                 AddColumnRoute::ExternalIndividual => {
    530                     selections.push(Selection::Keyword(Keyword::ExternalIndividualSelection))
    531                 }
    532             }
    533         }
    534         Route::Support => selections.push(Selection::Keyword(Keyword::Support)),
    535         Route::NewDeck => {
    536             selections.push(Selection::Keyword(Keyword::Deck));
    537             selections.push(Selection::Keyword(Keyword::New));
    538         }
    539         Route::EditDeck(index) => {
    540             selections.push(Selection::Keyword(Keyword::Deck));
    541             selections.push(Selection::Keyword(Keyword::Edit));
    542             selections.push(Selection::Payload(index.to_string()));
    543         }
    544     }
    545 
    546     if selections.is_empty() {
    547         None
    548     } else {
    549         Some(
    550             selections
    551                 .iter()
    552                 .map(|k| k.to_string())
    553                 .collect::<Vec<String>>()
    554                 .join(":"),
    555         )
    556     }
    557 }
    558 
    559 fn generate_pubkey_selections(source: &PubkeySource) -> Vec<Selection> {
    560     let mut selections = Vec::new();
    561     match source {
    562         PubkeySource::Explicit(pubkey) => {
    563             selections.push(Selection::Keyword(Keyword::Explicit));
    564             selections.push(Selection::Payload(pubkey.hex()));
    565         }
    566         PubkeySource::DeckAuthor => {
    567             selections.push(Selection::Keyword(Keyword::DeckAuthor));
    568         }
    569     }
    570     selections
    571 }
    572 
    573 impl Selection {
    574     fn from_serialized(serialized: &str) -> Vec<Self> {
    575         let mut selections = Vec::new();
    576         let seperator = ":";
    577 
    578         let mut serialized_copy = serialized.to_string();
    579         let mut buffer = serialized_copy.as_mut();
    580 
    581         let mut next_is_payload = false;
    582         while let Some(index) = buffer.find(seperator) {
    583             if let Ok(keyword) = Keyword::from_str(&buffer[..index]) {
    584                 selections.push(Selection::Keyword(keyword.clone()));
    585                 if keyword.has_payload() {
    586                     next_is_payload = true;
    587                 }
    588             }
    589 
    590             buffer = &mut buffer[index + seperator.len()..];
    591         }
    592 
    593         if next_is_payload {
    594             selections.push(Selection::Payload(buffer.to_string()));
    595         } else if let Ok(keyword) = Keyword::from_str(buffer) {
    596             selections.push(Selection::Keyword(keyword.clone()));
    597         }
    598 
    599         selections
    600     }
    601 }
    602 
    603 fn selections_to_route(selections: Vec<Selection>) -> Option<CleanIntermediaryRoute> {
    604     match selections.first()? {
    605         Selection::Keyword(Keyword::Contact) => match selections.get(1)? {
    606             Selection::Keyword(Keyword::Explicit) => {
    607                 if let Selection::Payload(hex) = selections.get(2)? {
    608                     Some(CleanIntermediaryRoute::ToTimeline(
    609                         TimelineKind::contact_list(PubkeySource::Explicit(
    610                             Pubkey::from_hex(hex.as_str()).ok()?,
    611                         )),
    612                     ))
    613                 } else {
    614                     None
    615                 }
    616             }
    617             Selection::Keyword(Keyword::DeckAuthor) => Some(CleanIntermediaryRoute::ToTimeline(
    618                 TimelineKind::contact_list(PubkeySource::DeckAuthor),
    619             )),
    620             _ => None,
    621         },
    622         Selection::Keyword(Keyword::Notifs) => match selections.get(1)? {
    623             Selection::Keyword(Keyword::Explicit) => {
    624                 if let Selection::Payload(hex) = selections.get(2)? {
    625                     Some(CleanIntermediaryRoute::ToTimeline(
    626                         TimelineKind::notifications(PubkeySource::Explicit(
    627                             Pubkey::from_hex(hex.as_str()).ok()?,
    628                         )),
    629                     ))
    630                 } else {
    631                     None
    632                 }
    633             }
    634             Selection::Keyword(Keyword::DeckAuthor) => Some(CleanIntermediaryRoute::ToTimeline(
    635                 TimelineKind::notifications(PubkeySource::DeckAuthor),
    636             )),
    637             _ => None,
    638         },
    639         Selection::Keyword(Keyword::Profile) => match selections.get(1)? {
    640             Selection::Keyword(Keyword::Explicit) => {
    641                 if let Selection::Payload(hex) = selections.get(2)? {
    642                     Some(CleanIntermediaryRoute::ToTimeline(TimelineKind::profile(
    643                         PubkeySource::Explicit(Pubkey::from_hex(hex.as_str()).ok()?),
    644                     )))
    645                 } else {
    646                     None
    647                 }
    648             }
    649             Selection::Keyword(Keyword::DeckAuthor) => Some(CleanIntermediaryRoute::ToTimeline(
    650                 TimelineKind::profile(PubkeySource::DeckAuthor),
    651             )),
    652             _ => None,
    653         },
    654         Selection::Keyword(Keyword::Universe) => {
    655             Some(CleanIntermediaryRoute::ToTimeline(TimelineKind::Universe))
    656         }
    657         Selection::Keyword(Keyword::Hashtag) => {
    658             if let Selection::Payload(hashtag) = selections.get(1)? {
    659                 Some(CleanIntermediaryRoute::ToTimeline(TimelineKind::Hashtag(
    660                     hashtag.to_string(),
    661                 )))
    662             } else {
    663                 None
    664             }
    665         }
    666         Selection::Keyword(Keyword::Generic) => {
    667             Some(CleanIntermediaryRoute::ToTimeline(TimelineKind::Generic))
    668         }
    669         Selection::Keyword(Keyword::Thread) => {
    670             if let Selection::Payload(hex) = selections.get(1)? {
    671                 Some(CleanIntermediaryRoute::ToRoute(Route::thread(
    672                     NoteId::from_hex(hex.as_str()).ok()?,
    673                 )))
    674             } else {
    675                 None
    676             }
    677         }
    678         Selection::Keyword(Keyword::Reply) => {
    679             if let Selection::Payload(hex) = selections.get(1)? {
    680                 Some(CleanIntermediaryRoute::ToRoute(Route::reply(
    681                     NoteId::from_hex(hex.as_str()).ok()?,
    682                 )))
    683             } else {
    684                 None
    685             }
    686         }
    687         Selection::Keyword(Keyword::Quote) => {
    688             if let Selection::Payload(hex) = selections.get(1)? {
    689                 Some(CleanIntermediaryRoute::ToRoute(Route::quote(
    690                     NoteId::from_hex(hex.as_str()).ok()?,
    691                 )))
    692             } else {
    693                 None
    694             }
    695         }
    696         Selection::Keyword(Keyword::Account) => match selections.get(1)? {
    697             Selection::Keyword(Keyword::Show) => Some(CleanIntermediaryRoute::ToRoute(
    698                 Route::Accounts(AccountsRoute::Accounts),
    699             )),
    700             Selection::Keyword(Keyword::New) => Some(CleanIntermediaryRoute::ToRoute(
    701                 Route::Accounts(AccountsRoute::AddAccount),
    702             )),
    703             _ => None,
    704         },
    705         Selection::Keyword(Keyword::Relay) => Some(CleanIntermediaryRoute::ToRoute(Route::Relays)),
    706         Selection::Keyword(Keyword::Compose) => {
    707             Some(CleanIntermediaryRoute::ToRoute(Route::ComposeNote))
    708         }
    709         Selection::Keyword(Keyword::Column) => match selections.get(1)? {
    710             Selection::Keyword(Keyword::NotificationSelection) => {
    711                 Some(CleanIntermediaryRoute::ToRoute(Route::AddColumn(
    712                     AddColumnRoute::UndecidedNotification,
    713                 )))
    714             }
    715             Selection::Keyword(Keyword::ExternalNotifSelection) => {
    716                 Some(CleanIntermediaryRoute::ToRoute(Route::AddColumn(
    717                     AddColumnRoute::ExternalNotification,
    718                 )))
    719             }
    720             Selection::Keyword(Keyword::HashtagSelection) => Some(CleanIntermediaryRoute::ToRoute(
    721                 Route::AddColumn(AddColumnRoute::Hashtag),
    722             )),
    723             Selection::Keyword(Keyword::IndividualSelection) => {
    724                 Some(CleanIntermediaryRoute::ToRoute(Route::AddColumn(
    725                     AddColumnRoute::UndecidedIndividual,
    726                 )))
    727             }
    728             Selection::Keyword(Keyword::ExternalIndividualSelection) => {
    729                 Some(CleanIntermediaryRoute::ToRoute(Route::AddColumn(
    730                     AddColumnRoute::ExternalIndividual,
    731                 )))
    732             }
    733             _ => None,
    734         },
    735         Selection::Keyword(Keyword::Support) => {
    736             Some(CleanIntermediaryRoute::ToRoute(Route::Support))
    737         }
    738         Selection::Keyword(Keyword::Deck) => match selections.get(1)? {
    739             Selection::Keyword(Keyword::New) => {
    740                 Some(CleanIntermediaryRoute::ToRoute(Route::NewDeck))
    741             }
    742             Selection::Keyword(Keyword::Edit) => {
    743                 if let Selection::Payload(index_str) = selections.get(2)? {
    744                     let parsed_index = index_str.parse::<usize>().ok()?;
    745                     Some(CleanIntermediaryRoute::ToRoute(Route::EditDeck(
    746                         parsed_index,
    747                     )))
    748                 } else {
    749                     None
    750                 }
    751             }
    752             _ => None,
    753         },
    754         Selection::Payload(_)
    755         | Selection::Keyword(Keyword::Explicit)
    756         | Selection::Keyword(Keyword::New)
    757         | Selection::Keyword(Keyword::DeckAuthor)
    758         | Selection::Keyword(Keyword::Show)
    759         | Selection::Keyword(Keyword::NotificationSelection)
    760         | Selection::Keyword(Keyword::ExternalNotifSelection)
    761         | Selection::Keyword(Keyword::HashtagSelection)
    762         | Selection::Keyword(Keyword::IndividualSelection)
    763         | Selection::Keyword(Keyword::ExternalIndividualSelection)
    764         | Selection::Keyword(Keyword::Edit) => None,
    765     }
    766 }
    767 
    768 impl fmt::Display for Selection {
    769     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    770         match self {
    771             Selection::Keyword(keyword) => write!(f, "{}", keyword),
    772             Selection::Payload(payload) => write!(f, "{}", payload),
    773         }
    774     }
    775 }
    776 
    777 #[cfg(test)]
    778 mod tests {
    779     //use enostr::Pubkey;
    780 
    781     //use crate::{route::Route, timeline::TimelineRoute};
    782 
    783     //use super::deserialize_columns;
    784 
    785     /* TODO: re-enable once we have test_app working again
    786     #[test]
    787     fn test_deserialize_columns() {
    788         let serialized = vec![
    789             vec!["universe".to_owned()],
    790             vec![
    791                 "notifs:explicit:aa733081e4f0f79dd43023d8983265593f2b41a988671cfcef3f489b91ad93fe"
    792                     .to_owned(),
    793             ],
    794         ];
    795 
    796         let user =
    797             Pubkey::from_hex("aa733081e4f0f79dd43023d8983265593f2b41a988671cfcef3f489b91ad93fe")
    798                 .unwrap();
    799 
    800         let app = test_app();
    801         let cols = deserialize_columns(&app.ndb, user.bytes(), serialized);
    802 
    803         assert_eq!(cols.columns().len(), 2);
    804         let router = cols.column(0).router();
    805         assert_eq!(router.routes().len(), 1);
    806 
    807         if let Route::Timeline(TimelineRoute::Timeline(_)) = router.routes().first().unwrap() {
    808         } else {
    809             panic!("The first router route is not a TimelineRoute::Timeline variant");
    810         }
    811 
    812         let router = cols.column(1).router();
    813         assert_eq!(router.routes().len(), 1);
    814         if let Route::Timeline(TimelineRoute::Timeline(_)) = router.routes().first().unwrap() {
    815         } else {
    816             panic!("The second router route is not a TimelineRoute::Timeline variant");
    817         }
    818     }
    819     */
    820 }