notedeck

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

decks.rs (29341B)


      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, false),
    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::Thread(root_id) => {
    477                             selections.push(Selection::Keyword(Keyword::Thread));
    478                             selections.push(Selection::Payload(hex::encode(root_id.bytes())));
    479                         }
    480                         TimelineKind::Generic => {
    481                             selections.push(Selection::Keyword(Keyword::Generic))
    482                         }
    483                         TimelineKind::Hashtag(hashtag) => {
    484                             selections.push(Selection::Keyword(Keyword::Hashtag));
    485                             selections.push(Selection::Payload(hashtag.to_string()));
    486                         }
    487                     }
    488                 }
    489             }
    490             TimelineRoute::Thread(note_id) => {
    491                 selections.push(Selection::Keyword(Keyword::Thread));
    492                 selections.push(Selection::Payload(note_id.hex()));
    493             }
    494             TimelineRoute::Profile(pubkey) => {
    495                 selections.push(Selection::Keyword(Keyword::Profile));
    496                 selections.push(Selection::Keyword(Keyword::Explicit));
    497                 selections.push(Selection::Payload(pubkey.hex()));
    498             }
    499             TimelineRoute::Reply(note_id) => {
    500                 selections.push(Selection::Keyword(Keyword::Reply));
    501                 selections.push(Selection::Payload(note_id.hex()));
    502             }
    503             TimelineRoute::Quote(note_id) => {
    504                 selections.push(Selection::Keyword(Keyword::Quote));
    505                 selections.push(Selection::Payload(note_id.hex()));
    506             }
    507         },
    508         Route::Accounts(accounts_route) => {
    509             selections.push(Selection::Keyword(Keyword::Account));
    510             match accounts_route {
    511                 AccountsRoute::Accounts => selections.push(Selection::Keyword(Keyword::Show)),
    512                 AccountsRoute::AddAccount => selections.push(Selection::Keyword(Keyword::New)),
    513             }
    514         }
    515         Route::Relays => selections.push(Selection::Keyword(Keyword::Relay)),
    516         Route::ComposeNote => selections.push(Selection::Keyword(Keyword::Compose)),
    517         Route::AddColumn(add_column_route) => {
    518             selections.push(Selection::Keyword(Keyword::Column));
    519             match add_column_route {
    520                 AddColumnRoute::Base => (),
    521                 AddColumnRoute::UndecidedNotification => {
    522                     selections.push(Selection::Keyword(Keyword::NotificationSelection))
    523                 }
    524                 AddColumnRoute::ExternalNotification => {
    525                     selections.push(Selection::Keyword(Keyword::ExternalNotifSelection))
    526                 }
    527                 AddColumnRoute::Hashtag => {
    528                     selections.push(Selection::Keyword(Keyword::HashtagSelection))
    529                 }
    530                 AddColumnRoute::UndecidedIndividual => {
    531                     selections.push(Selection::Keyword(Keyword::IndividualSelection))
    532                 }
    533                 AddColumnRoute::ExternalIndividual => {
    534                     selections.push(Selection::Keyword(Keyword::ExternalIndividualSelection))
    535                 }
    536             }
    537         }
    538         Route::Support => selections.push(Selection::Keyword(Keyword::Support)),
    539         Route::NewDeck => {
    540             selections.push(Selection::Keyword(Keyword::Deck));
    541             selections.push(Selection::Keyword(Keyword::New));
    542         }
    543         Route::EditDeck(index) => {
    544             selections.push(Selection::Keyword(Keyword::Deck));
    545             selections.push(Selection::Keyword(Keyword::Edit));
    546             selections.push(Selection::Payload(index.to_string()));
    547         }
    548         Route::EditProfile(pubkey) => {
    549             selections.push(Selection::Keyword(Keyword::Profile));
    550             selections.push(Selection::Keyword(Keyword::Edit));
    551             selections.push(Selection::Payload(pubkey.hex()));
    552         }
    553     }
    554 
    555     if selections.is_empty() {
    556         None
    557     } else {
    558         Some(
    559             selections
    560                 .iter()
    561                 .map(|k| k.to_string())
    562                 .collect::<Vec<String>>()
    563                 .join(":"),
    564         )
    565     }
    566 }
    567 
    568 fn generate_pubkey_selections(source: &PubkeySource) -> Vec<Selection> {
    569     let mut selections = Vec::new();
    570     match source {
    571         PubkeySource::Explicit(pubkey) => {
    572             selections.push(Selection::Keyword(Keyword::Explicit));
    573             selections.push(Selection::Payload(pubkey.hex()));
    574         }
    575         PubkeySource::DeckAuthor => {
    576             selections.push(Selection::Keyword(Keyword::DeckAuthor));
    577         }
    578     }
    579     selections
    580 }
    581 
    582 impl Selection {
    583     fn from_serialized(serialized: &str) -> Vec<Self> {
    584         let mut selections = Vec::new();
    585         let seperator = ":";
    586 
    587         let mut serialized_copy = serialized.to_string();
    588         let mut buffer = serialized_copy.as_mut();
    589 
    590         let mut next_is_payload = false;
    591         while let Some(index) = buffer.find(seperator) {
    592             if let Ok(keyword) = Keyword::from_str(&buffer[..index]) {
    593                 selections.push(Selection::Keyword(keyword.clone()));
    594                 if keyword.has_payload() {
    595                     next_is_payload = true;
    596                 }
    597             }
    598 
    599             buffer = &mut buffer[index + seperator.len()..];
    600         }
    601 
    602         if next_is_payload {
    603             selections.push(Selection::Payload(buffer.to_string()));
    604         } else if let Ok(keyword) = Keyword::from_str(buffer) {
    605             selections.push(Selection::Keyword(keyword.clone()));
    606         }
    607 
    608         selections
    609     }
    610 }
    611 
    612 fn selections_to_route(selections: Vec<Selection>) -> Option<CleanIntermediaryRoute> {
    613     match selections.first()? {
    614         Selection::Keyword(Keyword::Contact) => match selections.get(1)? {
    615             Selection::Keyword(Keyword::Explicit) => {
    616                 if let Selection::Payload(hex) = selections.get(2)? {
    617                     Some(CleanIntermediaryRoute::ToTimeline(
    618                         TimelineKind::contact_list(PubkeySource::Explicit(
    619                             Pubkey::from_hex(hex.as_str()).ok()?,
    620                         )),
    621                     ))
    622                 } else {
    623                     None
    624                 }
    625             }
    626             Selection::Keyword(Keyword::DeckAuthor) => Some(CleanIntermediaryRoute::ToTimeline(
    627                 TimelineKind::contact_list(PubkeySource::DeckAuthor),
    628             )),
    629             _ => None,
    630         },
    631         Selection::Keyword(Keyword::Notifs) => match selections.get(1)? {
    632             Selection::Keyword(Keyword::Explicit) => {
    633                 if let Selection::Payload(hex) = selections.get(2)? {
    634                     Some(CleanIntermediaryRoute::ToTimeline(
    635                         TimelineKind::notifications(PubkeySource::Explicit(
    636                             Pubkey::from_hex(hex.as_str()).ok()?,
    637                         )),
    638                     ))
    639                 } else {
    640                     None
    641                 }
    642             }
    643             Selection::Keyword(Keyword::DeckAuthor) => Some(CleanIntermediaryRoute::ToTimeline(
    644                 TimelineKind::notifications(PubkeySource::DeckAuthor),
    645             )),
    646             _ => None,
    647         },
    648         Selection::Keyword(Keyword::Profile) => match selections.get(1)? {
    649             Selection::Keyword(Keyword::Explicit) => {
    650                 if let Selection::Payload(hex) = selections.get(2)? {
    651                     Some(CleanIntermediaryRoute::ToTimeline(TimelineKind::profile(
    652                         PubkeySource::Explicit(Pubkey::from_hex(hex.as_str()).ok()?),
    653                     )))
    654                 } else {
    655                     None
    656                 }
    657             }
    658             Selection::Keyword(Keyword::DeckAuthor) => Some(CleanIntermediaryRoute::ToTimeline(
    659                 TimelineKind::profile(PubkeySource::DeckAuthor),
    660             )),
    661             Selection::Keyword(Keyword::Edit) => {
    662                 if let Selection::Payload(hex) = selections.get(2)? {
    663                     Some(CleanIntermediaryRoute::ToRoute(Route::EditProfile(
    664                         Pubkey::from_hex(hex.as_str()).ok()?,
    665                     )))
    666                 } else {
    667                     None
    668                 }
    669             }
    670             _ => None,
    671         },
    672         Selection::Keyword(Keyword::Universe) => {
    673             Some(CleanIntermediaryRoute::ToTimeline(TimelineKind::Universe))
    674         }
    675         Selection::Keyword(Keyword::Hashtag) => {
    676             if let Selection::Payload(hashtag) = selections.get(1)? {
    677                 Some(CleanIntermediaryRoute::ToTimeline(TimelineKind::Hashtag(
    678                     hashtag.to_string(),
    679                 )))
    680             } else {
    681                 None
    682             }
    683         }
    684         Selection::Keyword(Keyword::Generic) => {
    685             Some(CleanIntermediaryRoute::ToTimeline(TimelineKind::Generic))
    686         }
    687         Selection::Keyword(Keyword::Thread) => {
    688             if let Selection::Payload(hex) = selections.get(1)? {
    689                 Some(CleanIntermediaryRoute::ToRoute(Route::thread(
    690                     NoteId::from_hex(hex.as_str()).ok()?,
    691                 )))
    692             } else {
    693                 None
    694             }
    695         }
    696         Selection::Keyword(Keyword::Reply) => {
    697             if let Selection::Payload(hex) = selections.get(1)? {
    698                 Some(CleanIntermediaryRoute::ToRoute(Route::reply(
    699                     NoteId::from_hex(hex.as_str()).ok()?,
    700                 )))
    701             } else {
    702                 None
    703             }
    704         }
    705         Selection::Keyword(Keyword::Quote) => {
    706             if let Selection::Payload(hex) = selections.get(1)? {
    707                 Some(CleanIntermediaryRoute::ToRoute(Route::quote(
    708                     NoteId::from_hex(hex.as_str()).ok()?,
    709                 )))
    710             } else {
    711                 None
    712             }
    713         }
    714         Selection::Keyword(Keyword::Account) => match selections.get(1)? {
    715             Selection::Keyword(Keyword::Show) => Some(CleanIntermediaryRoute::ToRoute(
    716                 Route::Accounts(AccountsRoute::Accounts),
    717             )),
    718             Selection::Keyword(Keyword::New) => Some(CleanIntermediaryRoute::ToRoute(
    719                 Route::Accounts(AccountsRoute::AddAccount),
    720             )),
    721             _ => None,
    722         },
    723         Selection::Keyword(Keyword::Relay) => Some(CleanIntermediaryRoute::ToRoute(Route::Relays)),
    724         Selection::Keyword(Keyword::Compose) => {
    725             Some(CleanIntermediaryRoute::ToRoute(Route::ComposeNote))
    726         }
    727         Selection::Keyword(Keyword::Column) => match selections.get(1)? {
    728             Selection::Keyword(Keyword::NotificationSelection) => {
    729                 Some(CleanIntermediaryRoute::ToRoute(Route::AddColumn(
    730                     AddColumnRoute::UndecidedNotification,
    731                 )))
    732             }
    733             Selection::Keyword(Keyword::ExternalNotifSelection) => {
    734                 Some(CleanIntermediaryRoute::ToRoute(Route::AddColumn(
    735                     AddColumnRoute::ExternalNotification,
    736                 )))
    737             }
    738             Selection::Keyword(Keyword::HashtagSelection) => Some(CleanIntermediaryRoute::ToRoute(
    739                 Route::AddColumn(AddColumnRoute::Hashtag),
    740             )),
    741             Selection::Keyword(Keyword::IndividualSelection) => {
    742                 Some(CleanIntermediaryRoute::ToRoute(Route::AddColumn(
    743                     AddColumnRoute::UndecidedIndividual,
    744                 )))
    745             }
    746             Selection::Keyword(Keyword::ExternalIndividualSelection) => {
    747                 Some(CleanIntermediaryRoute::ToRoute(Route::AddColumn(
    748                     AddColumnRoute::ExternalIndividual,
    749                 )))
    750             }
    751             _ => None,
    752         },
    753         Selection::Keyword(Keyword::Support) => {
    754             Some(CleanIntermediaryRoute::ToRoute(Route::Support))
    755         }
    756         Selection::Keyword(Keyword::Deck) => match selections.get(1)? {
    757             Selection::Keyword(Keyword::New) => {
    758                 Some(CleanIntermediaryRoute::ToRoute(Route::NewDeck))
    759             }
    760             Selection::Keyword(Keyword::Edit) => {
    761                 if let Selection::Payload(index_str) = selections.get(2)? {
    762                     let parsed_index = index_str.parse::<usize>().ok()?;
    763                     Some(CleanIntermediaryRoute::ToRoute(Route::EditDeck(
    764                         parsed_index,
    765                     )))
    766                 } else {
    767                     None
    768                 }
    769             }
    770             _ => None,
    771         },
    772         Selection::Payload(_)
    773         | Selection::Keyword(Keyword::Explicit)
    774         | Selection::Keyword(Keyword::New)
    775         | Selection::Keyword(Keyword::DeckAuthor)
    776         | Selection::Keyword(Keyword::Show)
    777         | Selection::Keyword(Keyword::NotificationSelection)
    778         | Selection::Keyword(Keyword::ExternalNotifSelection)
    779         | Selection::Keyword(Keyword::HashtagSelection)
    780         | Selection::Keyword(Keyword::IndividualSelection)
    781         | Selection::Keyword(Keyword::ExternalIndividualSelection)
    782         | Selection::Keyword(Keyword::Edit) => None,
    783     }
    784 }
    785 
    786 impl fmt::Display for Selection {
    787     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    788         match self {
    789             Selection::Keyword(keyword) => write!(f, "{}", keyword),
    790             Selection::Payload(payload) => write!(f, "{}", payload),
    791         }
    792     }
    793 }
    794 
    795 #[cfg(test)]
    796 mod tests {
    797     //use enostr::Pubkey;
    798 
    799     //use crate::{route::Route, timeline::TimelineRoute};
    800 
    801     //use super::deserialize_columns;
    802 
    803     /* TODO: re-enable once we have test_app working again
    804     #[test]
    805     fn test_deserialize_columns() {
    806         let serialized = vec![
    807             vec!["universe".to_owned()],
    808             vec![
    809                 "notifs:explicit:aa733081e4f0f79dd43023d8983265593f2b41a988671cfcef3f489b91ad93fe"
    810                     .to_owned(),
    811             ],
    812         ];
    813 
    814         let user =
    815             Pubkey::from_hex("aa733081e4f0f79dd43023d8983265593f2b41a988671cfcef3f489b91ad93fe")
    816                 .unwrap();
    817 
    818         let app = test_app();
    819         let cols = deserialize_columns(&app.ndb, user.bytes(), serialized);
    820 
    821         assert_eq!(cols.columns().len(), 2);
    822         let router = cols.column(0).router();
    823         assert_eq!(router.routes().len(), 1);
    824 
    825         if let Route::Timeline(TimelineRoute::Timeline(_)) = router.routes().first().unwrap() {
    826         } else {
    827             panic!("The first router route is not a TimelineRoute::Timeline variant");
    828         }
    829 
    830         let router = cols.column(1).router();
    831         assert_eq!(router.routes().len(), 1);
    832         if let Route::Timeline(TimelineRoute::Timeline(_)) = router.routes().first().unwrap() {
    833         } else {
    834             panic!("The second router route is not a TimelineRoute::Timeline variant");
    835         }
    836     }
    837     */
    838 }