commit 34f0c3b0ce052b234ddc4683cc7ee5b52254ecdd
parent 4cd3515a787049a671d69de960ceb9e22d4f700f
Author: kernelkind <kernelkind@gmail.com>
Date: Thu, 5 Dec 2024 18:42:39 -0500
serialization for DecksCache
Signed-off-by: kernelkind <kernelkind@gmail.com>
Diffstat:
A | src/storage/decks.rs | | | 799 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
1 file changed, 799 insertions(+), 0 deletions(-)
diff --git a/src/storage/decks.rs b/src/storage/decks.rs
@@ -0,0 +1,799 @@
+use std::{collections::HashMap, fmt, str::FromStr};
+
+use enostr::{NoteId, Pubkey};
+use nostrdb::Ndb;
+use serde::{Deserialize, Serialize};
+use tracing::{error, info};
+
+use crate::{
+ accounts::AccountsRoute,
+ column::{Columns, IntermediaryRoute},
+ decks::{Deck, Decks, DecksCache},
+ route::Route,
+ timeline::{kind::ListKind, PubkeySource, TimelineKind, TimelineRoute},
+ ui::add_column::AddColumnRoute,
+ Error,
+};
+
+use super::{write_file, DataPath, DataPathType, Directory};
+
+pub static DECKS_CACHE_FILE: &str = "decks_cache.json";
+
+pub fn load_decks_cache(path: &DataPath, ndb: &Ndb) -> Option<DecksCache> {
+ let data_path = path.path(DataPathType::Setting);
+
+ let decks_cache_str = match Directory::new(data_path).get_file(DECKS_CACHE_FILE.to_owned()) {
+ Ok(s) => s,
+ Err(e) => {
+ error!(
+ "Could not read decks cache from file {}: {}",
+ DECKS_CACHE_FILE, e
+ );
+ return None;
+ }
+ };
+
+ let serializable_decks_cache =
+ serde_json::from_str::<SerializableDecksCache>(&decks_cache_str).ok()?;
+
+ serializable_decks_cache.decks_cache(ndb).ok()
+}
+
+pub fn save_decks_cache(path: &DataPath, decks_cache: &DecksCache) {
+ let serialized_decks_cache =
+ match serde_json::to_string(&SerializableDecksCache::to_serializable(decks_cache)) {
+ Ok(s) => s,
+ Err(e) => {
+ error!("Could not serialize decks cache: {}", e);
+ return;
+ }
+ };
+
+ let data_path = path.path(DataPathType::Setting);
+
+ if let Err(e) = write_file(
+ &data_path,
+ DECKS_CACHE_FILE.to_string(),
+ &serialized_decks_cache,
+ ) {
+ error!(
+ "Could not write decks cache to file {}: {}",
+ DECKS_CACHE_FILE, e
+ );
+ } else {
+ info!("Successfully wrote decks cache to {}", DECKS_CACHE_FILE);
+ }
+}
+
+#[derive(Serialize, Deserialize)]
+struct SerializableDecksCache {
+ #[serde(serialize_with = "serialize_map", deserialize_with = "deserialize_map")]
+ decks_cache: HashMap<Pubkey, SerializableDecks>,
+}
+
+impl SerializableDecksCache {
+ fn to_serializable(decks_cache: &DecksCache) -> Self {
+ SerializableDecksCache {
+ decks_cache: decks_cache
+ .get_mapping()
+ .iter()
+ .map(|(k, v)| (*k, SerializableDecks::from_decks(v)))
+ .collect(),
+ }
+ }
+
+ pub fn decks_cache(self, ndb: &Ndb) -> Result<DecksCache, Error> {
+ let account_to_decks = self
+ .decks_cache
+ .into_iter()
+ .map(|(pubkey, serializable_decks)| {
+ let deck_key = pubkey.bytes();
+ serializable_decks
+ .decks(ndb, deck_key)
+ .map(|decks| (pubkey, decks))
+ })
+ .collect::<Result<HashMap<Pubkey, Decks>, Error>>()?;
+
+ Ok(DecksCache::new(account_to_decks))
+ }
+}
+
+fn serialize_map<S>(
+ map: &HashMap<Pubkey, SerializableDecks>,
+ serializer: S,
+) -> Result<S::Ok, S::Error>
+where
+ S: serde::Serializer,
+{
+ let stringified_map: HashMap<String, &SerializableDecks> =
+ map.iter().map(|(k, v)| (k.hex(), v)).collect();
+ stringified_map.serialize(serializer)
+}
+
+fn deserialize_map<'de, D>(deserializer: D) -> Result<HashMap<Pubkey, SerializableDecks>, D::Error>
+where
+ D: serde::Deserializer<'de>,
+{
+ let stringified_map: HashMap<String, SerializableDecks> = HashMap::deserialize(deserializer)?;
+
+ stringified_map
+ .into_iter()
+ .map(|(k, v)| {
+ let key = Pubkey::from_hex(&k).map_err(serde::de::Error::custom)?;
+ Ok((key, v))
+ })
+ .collect()
+}
+
+#[derive(Serialize, Deserialize)]
+struct SerializableDecks {
+ active_deck: usize,
+ decks: Vec<SerializableDeck>,
+}
+
+impl SerializableDecks {
+ pub fn from_decks(decks: &Decks) -> Self {
+ Self {
+ active_deck: decks.active_index(),
+ decks: decks
+ .decks()
+ .iter()
+ .map(SerializableDeck::from_deck)
+ .collect(),
+ }
+ }
+
+ fn decks(self, ndb: &Ndb, deck_key: &[u8; 32]) -> Result<Decks, Error> {
+ Ok(Decks::from_decks(
+ self.active_deck,
+ self.decks
+ .into_iter()
+ .map(|d| d.deck(ndb, deck_key))
+ .collect::<Result<_, _>>()?,
+ ))
+ }
+}
+
+#[derive(Serialize, Deserialize)]
+struct SerializableDeck {
+ metadata: Vec<String>,
+ columns: Vec<Vec<String>>,
+}
+
+#[derive(PartialEq, Clone)]
+enum MetadataKeyword {
+ Icon,
+ Name,
+}
+
+impl MetadataKeyword {
+ const MAPPING: &'static [(&'static str, MetadataKeyword)] = &[
+ ("icon", MetadataKeyword::Icon),
+ ("name", MetadataKeyword::Name),
+ ];
+}
+impl fmt::Display for MetadataKeyword {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let name = MetadataKeyword::MAPPING
+ .iter()
+ .find(|(_, keyword)| keyword == self)
+ .map(|(name, _)| *name)
+ .expect("MAPPING is incorrect");
+
+ write!(f, "{}", name)
+ }
+}
+
+impl FromStr for MetadataKeyword {
+ type Err = Error;
+
+ fn from_str(serialized: &str) -> Result<Self, Self::Err> {
+ MetadataKeyword::MAPPING
+ .iter()
+ .find(|(name, _)| *name == serialized)
+ .map(|(_, keyword)| keyword.clone())
+ .ok_or(Error::Generic(
+ "Could not convert string to Keyword enum".to_owned(),
+ ))
+ }
+}
+
+struct MetadataPayload {
+ keyword: MetadataKeyword,
+ value: String,
+}
+
+impl MetadataPayload {
+ fn new(keyword: MetadataKeyword, value: String) -> Self {
+ Self { keyword, value }
+ }
+}
+
+fn serialize_metadata(payloads: Vec<MetadataPayload>) -> Vec<String> {
+ payloads
+ .into_iter()
+ .map(|payload| format!("{}:{}", payload.keyword, payload.value))
+ .collect()
+}
+
+fn deserialize_metadata(serialized_metadatas: Vec<String>) -> Option<Vec<MetadataPayload>> {
+ let mut payloads = Vec::new();
+ for serialized_metadata in serialized_metadatas {
+ let cur_split: Vec<&str> = serialized_metadata.split(':').collect();
+ if cur_split.len() != 2 {
+ continue;
+ }
+
+ if let Ok(keyword) = MetadataKeyword::from_str(cur_split.first().unwrap()) {
+ payloads.push(MetadataPayload {
+ keyword,
+ value: cur_split.get(1).unwrap().to_string(),
+ });
+ }
+ }
+
+ if payloads.is_empty() {
+ None
+ } else {
+ Some(payloads)
+ }
+}
+
+impl SerializableDeck {
+ pub fn from_deck(deck: &Deck) -> Self {
+ let columns = serialize_columns(deck.columns());
+
+ let metadata = serialize_metadata(vec![
+ MetadataPayload::new(MetadataKeyword::Icon, deck.icon.to_string()),
+ MetadataPayload::new(MetadataKeyword::Name, deck.name.clone()),
+ ]);
+
+ SerializableDeck { metadata, columns }
+ }
+
+ pub fn deck(self, ndb: &Ndb, deck_user: &[u8; 32]) -> Result<Deck, Error> {
+ let columns = deserialize_columns(ndb, deck_user, self.columns);
+ let deserialized_metadata = deserialize_metadata(self.metadata)
+ .ok_or(Error::Generic("Could not deserialize metadata".to_owned()))?;
+
+ let icon = deserialized_metadata
+ .iter()
+ .find(|p| p.keyword == MetadataKeyword::Icon)
+ .map_or_else(|| "🇩", |f| &f.value);
+ let name = deserialized_metadata
+ .iter()
+ .find(|p| p.keyword == MetadataKeyword::Name)
+ .map_or_else(|| "Deck", |f| &f.value)
+ .to_string();
+
+ Ok(Deck::new_with_columns(
+ icon.parse::<char>()
+ .map_err(|_| Error::Generic("could not convert String -> char".to_owned()))?,
+ name,
+ columns,
+ ))
+ }
+}
+
+fn serialize_columns(columns: &Columns) -> Vec<Vec<String>> {
+ let mut cols_serialized: Vec<Vec<String>> = Vec::new();
+
+ for column in columns.columns() {
+ let mut column_routes = Vec::new();
+ for route in column.router().routes() {
+ if let Some(route_str) = serialize_route(route, columns) {
+ column_routes.push(route_str);
+ }
+ }
+ cols_serialized.push(column_routes);
+ }
+
+ cols_serialized
+}
+
+fn deserialize_columns(ndb: &Ndb, deck_user: &[u8; 32], serialized: Vec<Vec<String>>) -> Columns {
+ let mut cols = Columns::new();
+ for serialized_routes in serialized {
+ let mut cur_routes = Vec::new();
+ for serialized_route in serialized_routes {
+ let selections = Selection::from_serialized(&serialized_route);
+ if let Some(route_intermediary) = selections_to_route(selections.clone()) {
+ if let Some(ir) = route_intermediary.intermediary_route(ndb, Some(deck_user)) {
+ match &ir {
+ IntermediaryRoute::Route(Route::Timeline(TimelineRoute::Thread(_)))
+ | IntermediaryRoute::Route(Route::Timeline(TimelineRoute::Profile(_))) => {
+ // Do nothing. Threads & Profiles not yet supported for deserialization
+ }
+ IntermediaryRoute::Timeline(tl)
+ if matches!(tl.kind, TimelineKind::Profile(_)) =>
+ {
+ // Do nothing. Profiles aren't yet supported for deserialization
+ }
+ _ => cur_routes.push(ir),
+ }
+ }
+ } else {
+ error!(
+ "could not turn selections to RouteIntermediary: {:?}",
+ selections
+ );
+ }
+ }
+
+ if !cur_routes.is_empty() {
+ cols.insert_intermediary_routes(cur_routes);
+ }
+ }
+
+ cols
+}
+
+#[derive(Clone, Debug)]
+enum Selection {
+ Keyword(Keyword),
+ Payload(String),
+}
+
+#[derive(Clone, PartialEq, Debug)]
+enum Keyword {
+ Notifs,
+ Universe,
+ Contact,
+ Explicit,
+ DeckAuthor,
+ Profile,
+ Hashtag,
+ Generic,
+ Thread,
+ Reply,
+ Quote,
+ Account,
+ Show,
+ New,
+ Relay,
+ Compose,
+ Column,
+ NotificationSelection,
+ ExternalNotifSelection,
+ HashtagSelection,
+ Support,
+ Deck,
+ Edit,
+}
+
+impl Keyword {
+ const MAPPING: &'static [(&'static str, Keyword, bool)] = &[
+ ("notifs", Keyword::Notifs, false),
+ ("universe", Keyword::Universe, false),
+ ("contact", Keyword::Contact, false),
+ ("explicit", Keyword::Explicit, true),
+ ("deck_author", Keyword::DeckAuthor, false),
+ ("profile", Keyword::Profile, true),
+ ("hashtag", Keyword::Hashtag, true),
+ ("generic", Keyword::Generic, false),
+ ("thread", Keyword::Thread, true),
+ ("reply", Keyword::Reply, true),
+ ("quote", Keyword::Quote, true),
+ ("account", Keyword::Account, false),
+ ("show", Keyword::Show, false),
+ ("new", Keyword::New, false),
+ ("relay", Keyword::Relay, false),
+ ("compose", Keyword::Compose, false),
+ ("column", Keyword::Column, false),
+ (
+ "notification_selection",
+ Keyword::NotificationSelection,
+ false,
+ ),
+ (
+ "external_notif_selection",
+ Keyword::ExternalNotifSelection,
+ false,
+ ),
+ ("hashtag_selection", Keyword::HashtagSelection, false),
+ ("support", Keyword::Support, false),
+ ("deck", Keyword::Deck, false),
+ ("edit", Keyword::Edit, true),
+ ];
+
+ fn has_payload(&self) -> bool {
+ Keyword::MAPPING
+ .iter()
+ .find(|(_, keyword, _)| keyword == self)
+ .map(|(_, _, has_payload)| *has_payload)
+ .unwrap_or(false)
+ }
+}
+
+impl fmt::Display for Keyword {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let name = Keyword::MAPPING
+ .iter()
+ .find(|(_, keyword, _)| keyword == self)
+ .map(|(name, _, _)| *name)
+ .expect("MAPPING is incorrect");
+
+ write!(f, "{}", name)
+ }
+}
+
+impl FromStr for Keyword {
+ type Err = Error;
+
+ fn from_str(serialized: &str) -> Result<Self, Self::Err> {
+ Keyword::MAPPING
+ .iter()
+ .find(|(name, _, _)| *name == serialized)
+ .map(|(_, keyword, _)| keyword.clone())
+ .ok_or(Error::Generic(
+ "Could not convert string to Keyword enum".to_owned(),
+ ))
+ }
+}
+
+enum CleanIntermediaryRoute {
+ ToTimeline(TimelineKind),
+ ToRoute(Route),
+}
+
+impl CleanIntermediaryRoute {
+ fn intermediary_route(self, ndb: &Ndb, user: Option<&[u8; 32]>) -> Option<IntermediaryRoute> {
+ match self {
+ CleanIntermediaryRoute::ToTimeline(timeline_kind) => Some(IntermediaryRoute::Timeline(
+ timeline_kind.into_timeline(ndb, user)?,
+ )),
+ CleanIntermediaryRoute::ToRoute(route) => Some(IntermediaryRoute::Route(route)),
+ }
+ }
+}
+
+// TODO: The public-accessible version will be a subset of this
+fn serialize_route(route: &Route, columns: &Columns) -> Option<String> {
+ let mut selections: Vec<Selection> = Vec::new();
+ match route {
+ Route::Timeline(timeline_route) => match timeline_route {
+ TimelineRoute::Timeline(timeline_id) => {
+ if let Some(timeline) = columns.find_timeline(*timeline_id) {
+ match &timeline.kind {
+ TimelineKind::List(list_kind) => match list_kind {
+ ListKind::Contact(pubkey_source) => {
+ selections.push(Selection::Keyword(Keyword::Contact));
+ selections.extend(generate_pubkey_selections(pubkey_source));
+ }
+ },
+ TimelineKind::Notifications(pubkey_source) => {
+ selections.push(Selection::Keyword(Keyword::Notifs));
+ selections.extend(generate_pubkey_selections(pubkey_source));
+ }
+ TimelineKind::Profile(pubkey_source) => {
+ selections.push(Selection::Keyword(Keyword::Profile));
+ selections.extend(generate_pubkey_selections(pubkey_source));
+ }
+ TimelineKind::Universe => {
+ selections.push(Selection::Keyword(Keyword::Universe))
+ }
+ TimelineKind::Generic => {
+ selections.push(Selection::Keyword(Keyword::Generic))
+ }
+ TimelineKind::Hashtag(hashtag) => {
+ selections.push(Selection::Keyword(Keyword::Hashtag));
+ selections.push(Selection::Payload(hashtag.to_string()));
+ }
+ }
+ }
+ }
+ TimelineRoute::Thread(note_id) => {
+ selections.push(Selection::Keyword(Keyword::Thread));
+ selections.push(Selection::Payload(note_id.hex()));
+ }
+ TimelineRoute::Profile(pubkey) => {
+ selections.push(Selection::Keyword(Keyword::Profile));
+ selections.push(Selection::Keyword(Keyword::Explicit));
+ selections.push(Selection::Payload(pubkey.hex()));
+ }
+ TimelineRoute::Reply(note_id) => {
+ selections.push(Selection::Keyword(Keyword::Reply));
+ selections.push(Selection::Payload(note_id.hex()));
+ }
+ TimelineRoute::Quote(note_id) => {
+ selections.push(Selection::Keyword(Keyword::Quote));
+ selections.push(Selection::Payload(note_id.hex()));
+ }
+ },
+ Route::Accounts(accounts_route) => {
+ selections.push(Selection::Keyword(Keyword::Account));
+ match accounts_route {
+ AccountsRoute::Accounts => selections.push(Selection::Keyword(Keyword::Show)),
+ AccountsRoute::AddAccount => selections.push(Selection::Keyword(Keyword::New)),
+ }
+ }
+ Route::Relays => selections.push(Selection::Keyword(Keyword::Relay)),
+ Route::ComposeNote => selections.push(Selection::Keyword(Keyword::Compose)),
+ Route::AddColumn(add_column_route) => {
+ selections.push(Selection::Keyword(Keyword::Column));
+ match add_column_route {
+ AddColumnRoute::Base => (),
+ AddColumnRoute::UndecidedNotification => {
+ selections.push(Selection::Keyword(Keyword::NotificationSelection))
+ }
+ AddColumnRoute::ExternalNotification => {
+ selections.push(Selection::Keyword(Keyword::ExternalNotifSelection))
+ }
+ AddColumnRoute::Hashtag => {
+ selections.push(Selection::Keyword(Keyword::HashtagSelection))
+ }
+ }
+ }
+ Route::Support => selections.push(Selection::Keyword(Keyword::Support)),
+ Route::NewDeck => {
+ selections.push(Selection::Keyword(Keyword::Deck));
+ selections.push(Selection::Keyword(Keyword::New));
+ }
+ Route::EditDeck(index) => {
+ selections.push(Selection::Keyword(Keyword::Deck));
+ selections.push(Selection::Keyword(Keyword::Edit));
+ selections.push(Selection::Payload(index.to_string()));
+ }
+ }
+
+ if selections.is_empty() {
+ None
+ } else {
+ Some(
+ selections
+ .iter()
+ .map(|k| k.to_string())
+ .collect::<Vec<String>>()
+ .join(":"),
+ )
+ }
+}
+
+fn generate_pubkey_selections(source: &PubkeySource) -> Vec<Selection> {
+ let mut selections = Vec::new();
+ match source {
+ PubkeySource::Explicit(pubkey) => {
+ selections.push(Selection::Keyword(Keyword::Explicit));
+ selections.push(Selection::Payload(pubkey.hex()));
+ }
+ PubkeySource::DeckAuthor => {
+ selections.push(Selection::Keyword(Keyword::DeckAuthor));
+ }
+ }
+ selections
+}
+
+impl Selection {
+ fn from_serialized(serialized: &str) -> Vec<Self> {
+ let mut selections = Vec::new();
+ let seperator = ":";
+
+ let mut serialized_copy = serialized.to_string();
+ let mut buffer = serialized_copy.as_mut();
+
+ let mut next_is_payload = false;
+ while let Some(index) = buffer.find(seperator) {
+ if let Ok(keyword) = Keyword::from_str(&buffer[..index]) {
+ selections.push(Selection::Keyword(keyword.clone()));
+ if keyword.has_payload() {
+ next_is_payload = true;
+ }
+ }
+
+ buffer = &mut buffer[index + seperator.len()..];
+ }
+
+ if next_is_payload {
+ selections.push(Selection::Payload(buffer.to_string()));
+ } else if let Ok(keyword) = Keyword::from_str(buffer) {
+ selections.push(Selection::Keyword(keyword.clone()));
+ }
+
+ selections
+ }
+}
+
+fn selections_to_route(selections: Vec<Selection>) -> Option<CleanIntermediaryRoute> {
+ match selections.first()? {
+ Selection::Keyword(Keyword::Contact) => match selections.get(1)? {
+ Selection::Keyword(Keyword::Explicit) => {
+ if let Selection::Payload(hex) = selections.get(2)? {
+ Some(CleanIntermediaryRoute::ToTimeline(
+ TimelineKind::contact_list(PubkeySource::Explicit(
+ Pubkey::from_hex(hex.as_str()).ok()?,
+ )),
+ ))
+ } else {
+ None
+ }
+ }
+ Selection::Keyword(Keyword::DeckAuthor) => Some(CleanIntermediaryRoute::ToTimeline(
+ TimelineKind::contact_list(PubkeySource::DeckAuthor),
+ )),
+ _ => None,
+ },
+ Selection::Keyword(Keyword::Notifs) => match selections.get(1)? {
+ Selection::Keyword(Keyword::Explicit) => {
+ if let Selection::Payload(hex) = selections.get(2)? {
+ Some(CleanIntermediaryRoute::ToTimeline(
+ TimelineKind::notifications(PubkeySource::Explicit(
+ Pubkey::from_hex(hex.as_str()).ok()?,
+ )),
+ ))
+ } else {
+ None
+ }
+ }
+ Selection::Keyword(Keyword::DeckAuthor) => Some(CleanIntermediaryRoute::ToTimeline(
+ TimelineKind::notifications(PubkeySource::DeckAuthor),
+ )),
+ _ => None,
+ },
+ Selection::Keyword(Keyword::Profile) => match selections.get(1)? {
+ Selection::Keyword(Keyword::Explicit) => {
+ if let Selection::Payload(hex) = selections.get(2)? {
+ Some(CleanIntermediaryRoute::ToTimeline(TimelineKind::profile(
+ PubkeySource::Explicit(Pubkey::from_hex(hex.as_str()).ok()?),
+ )))
+ } else {
+ None
+ }
+ }
+ Selection::Keyword(Keyword::DeckAuthor) => Some(CleanIntermediaryRoute::ToTimeline(
+ TimelineKind::profile(PubkeySource::DeckAuthor),
+ )),
+ _ => None,
+ },
+ Selection::Keyword(Keyword::Universe) => {
+ Some(CleanIntermediaryRoute::ToTimeline(TimelineKind::Universe))
+ }
+ Selection::Keyword(Keyword::Hashtag) => {
+ if let Selection::Payload(hashtag) = selections.get(1)? {
+ Some(CleanIntermediaryRoute::ToTimeline(TimelineKind::Hashtag(
+ hashtag.to_string(),
+ )))
+ } else {
+ None
+ }
+ }
+ Selection::Keyword(Keyword::Generic) => {
+ Some(CleanIntermediaryRoute::ToTimeline(TimelineKind::Generic))
+ }
+ Selection::Keyword(Keyword::Thread) => {
+ if let Selection::Payload(hex) = selections.get(1)? {
+ Some(CleanIntermediaryRoute::ToRoute(Route::thread(
+ NoteId::from_hex(hex.as_str()).ok()?,
+ )))
+ } else {
+ None
+ }
+ }
+ Selection::Keyword(Keyword::Reply) => {
+ if let Selection::Payload(hex) = selections.get(1)? {
+ Some(CleanIntermediaryRoute::ToRoute(Route::reply(
+ NoteId::from_hex(hex.as_str()).ok()?,
+ )))
+ } else {
+ None
+ }
+ }
+ Selection::Keyword(Keyword::Quote) => {
+ if let Selection::Payload(hex) = selections.get(1)? {
+ Some(CleanIntermediaryRoute::ToRoute(Route::quote(
+ NoteId::from_hex(hex.as_str()).ok()?,
+ )))
+ } else {
+ None
+ }
+ }
+ Selection::Keyword(Keyword::Account) => match selections.get(1)? {
+ Selection::Keyword(Keyword::Show) => Some(CleanIntermediaryRoute::ToRoute(
+ Route::Accounts(AccountsRoute::Accounts),
+ )),
+ Selection::Keyword(Keyword::New) => Some(CleanIntermediaryRoute::ToRoute(
+ Route::Accounts(AccountsRoute::AddAccount),
+ )),
+ _ => None,
+ },
+ Selection::Keyword(Keyword::Relay) => Some(CleanIntermediaryRoute::ToRoute(Route::Relays)),
+ Selection::Keyword(Keyword::Compose) => {
+ Some(CleanIntermediaryRoute::ToRoute(Route::ComposeNote))
+ }
+ Selection::Keyword(Keyword::Column) => match selections.get(1)? {
+ Selection::Keyword(Keyword::NotificationSelection) => {
+ Some(CleanIntermediaryRoute::ToRoute(Route::AddColumn(
+ AddColumnRoute::UndecidedNotification,
+ )))
+ }
+ Selection::Keyword(Keyword::ExternalNotifSelection) => {
+ Some(CleanIntermediaryRoute::ToRoute(Route::AddColumn(
+ AddColumnRoute::ExternalNotification,
+ )))
+ }
+ Selection::Keyword(Keyword::HashtagSelection) => Some(CleanIntermediaryRoute::ToRoute(
+ Route::AddColumn(AddColumnRoute::Hashtag),
+ )),
+ _ => None,
+ },
+ Selection::Keyword(Keyword::Support) => {
+ Some(CleanIntermediaryRoute::ToRoute(Route::Support))
+ }
+ Selection::Keyword(Keyword::Deck) => match selections.get(1)? {
+ Selection::Keyword(Keyword::New) => {
+ Some(CleanIntermediaryRoute::ToRoute(Route::NewDeck))
+ }
+ Selection::Keyword(Keyword::Edit) => {
+ if let Selection::Payload(index_str) = selections.get(2)? {
+ let parsed_index = index_str.parse::<usize>().ok()?;
+ Some(CleanIntermediaryRoute::ToRoute(Route::EditDeck(
+ parsed_index,
+ )))
+ } else {
+ None
+ }
+ }
+ _ => None,
+ },
+ Selection::Payload(_)
+ | Selection::Keyword(Keyword::Explicit)
+ | Selection::Keyword(Keyword::New)
+ | Selection::Keyword(Keyword::DeckAuthor)
+ | Selection::Keyword(Keyword::Show)
+ | Selection::Keyword(Keyword::NotificationSelection)
+ | Selection::Keyword(Keyword::ExternalNotifSelection)
+ | Selection::Keyword(Keyword::HashtagSelection)
+ | Selection::Keyword(Keyword::Edit) => None,
+ }
+}
+
+impl fmt::Display for Selection {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Selection::Keyword(keyword) => write!(f, "{}", keyword),
+ Selection::Payload(payload) => write!(f, "{}", payload),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use enostr::Pubkey;
+
+ use crate::{route::Route, test_data::test_app, timeline::TimelineRoute};
+
+ use super::deserialize_columns;
+
+ #[test]
+ fn test_deserialize_columns() {
+ let serialized = vec![
+ vec!["universe".to_owned()],
+ vec![
+ "notifs:explicit:aa733081e4f0f79dd43023d8983265593f2b41a988671cfcef3f489b91ad93fe"
+ .to_owned(),
+ ],
+ ];
+
+ let user =
+ Pubkey::from_hex("aa733081e4f0f79dd43023d8983265593f2b41a988671cfcef3f489b91ad93fe")
+ .unwrap();
+
+ let app = test_app();
+ let cols = deserialize_columns(&app.ndb, user.bytes(), serialized);
+
+ assert_eq!(cols.columns().len(), 2);
+ let router = cols.column(0).router();
+ assert_eq!(router.routes().len(), 1);
+
+ if let Route::Timeline(TimelineRoute::Timeline(_)) = router.routes().first().unwrap() {
+ } else {
+ panic!("The first router route is not a TimelineRoute::Timeline variant");
+ }
+
+ let router = cols.column(1).router();
+ assert_eq!(router.routes().len(), 1);
+ if let Route::Timeline(TimelineRoute::Timeline(_)) = router.routes().first().unwrap() {
+ } else {
+ panic!("The second router route is not a TimelineRoute::Timeline variant");
+ }
+ }
+}