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 }