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 }