route.rs (23087B)
1 use enostr::{NoteId, Pubkey}; 2 use notedeck::{tr, Localization, NoteZapTargetOwned, RootNoteIdBuf, WalletType}; 3 use std::ops::Range; 4 5 use crate::{ 6 accounts::AccountsRoute, 7 timeline::{kind::ColumnTitle, ThreadSelection, TimelineKind}, 8 ui::add_column::{AddAlgoRoute, AddColumnRoute}, 9 }; 10 11 use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter}; 12 13 /// App routing. These describe different places you can go inside Notedeck. 14 #[derive(Clone, Eq, PartialEq, Debug)] 15 pub enum Route { 16 Timeline(TimelineKind), 17 Thread(ThreadSelection), 18 Accounts(AccountsRoute), 19 Reply(NoteId), 20 Quote(NoteId), 21 Relays, 22 Settings, 23 ComposeNote, 24 AddColumn(AddColumnRoute), 25 EditProfile(Pubkey), 26 Support, 27 NewDeck, 28 Search, 29 EditDeck(usize), 30 Wallet(WalletType), 31 CustomizeZapAmount(NoteZapTargetOwned), 32 } 33 34 impl Route { 35 pub fn timeline(timeline_kind: TimelineKind) -> Self { 36 Route::Timeline(timeline_kind) 37 } 38 39 pub fn timeline_id(&self) -> Option<&TimelineKind> { 40 if let Route::Timeline(tid) = self { 41 Some(tid) 42 } else { 43 None 44 } 45 } 46 47 pub fn relays() -> Self { 48 Route::Relays 49 } 50 51 pub fn settings() -> Self { 52 Route::Settings 53 } 54 55 pub fn thread(thread_selection: ThreadSelection) -> Self { 56 Route::Thread(thread_selection) 57 } 58 59 pub fn profile(pubkey: Pubkey) -> Self { 60 Route::Timeline(TimelineKind::profile(pubkey)) 61 } 62 63 pub fn reply(replying_to: NoteId) -> Self { 64 Route::Reply(replying_to) 65 } 66 67 pub fn quote(quoting: NoteId) -> Self { 68 Route::Quote(quoting) 69 } 70 71 pub fn accounts() -> Self { 72 Route::Accounts(AccountsRoute::Accounts) 73 } 74 75 pub fn add_account() -> Self { 76 Route::Accounts(AccountsRoute::AddAccount) 77 } 78 79 pub fn serialize_tokens(&self, writer: &mut TokenWriter) { 80 match self { 81 Route::Timeline(timeline_kind) => timeline_kind.serialize_tokens(writer), 82 Route::Thread(selection) => { 83 writer.write_token("thread"); 84 85 if let Some(reply) = selection.selected_note { 86 writer.write_token("root"); 87 writer.write_token(&NoteId::new(*selection.root_id.bytes()).hex()); 88 writer.write_token("reply"); 89 writer.write_token(&reply.hex()); 90 } else { 91 writer.write_token(&NoteId::new(*selection.root_id.bytes()).hex()); 92 } 93 } 94 Route::Accounts(routes) => routes.serialize_tokens(writer), 95 Route::AddColumn(routes) => routes.serialize_tokens(writer), 96 Route::Search => writer.write_token("search"), 97 Route::Reply(note_id) => { 98 writer.write_token("reply"); 99 writer.write_token(¬e_id.hex()); 100 } 101 Route::Quote(note_id) => { 102 writer.write_token("quote"); 103 writer.write_token(¬e_id.hex()); 104 } 105 Route::EditDeck(ind) => { 106 writer.write_token("deck"); 107 writer.write_token("edit"); 108 writer.write_token(&ind.to_string()); 109 } 110 Route::EditProfile(pubkey) => { 111 writer.write_token("profile"); 112 writer.write_token("edit"); 113 writer.write_token(&pubkey.hex()); 114 } 115 Route::Relays => { 116 writer.write_token("relay"); 117 } 118 Route::Settings => { 119 writer.write_token("settings"); 120 } 121 Route::ComposeNote => { 122 writer.write_token("compose"); 123 } 124 Route::Support => { 125 writer.write_token("support"); 126 } 127 Route::NewDeck => { 128 writer.write_token("deck"); 129 writer.write_token("new"); 130 } 131 Route::Wallet(_) => { 132 writer.write_token("wallet"); 133 } 134 Route::CustomizeZapAmount(_) => writer.write_token("customize zap amount"), 135 } 136 } 137 138 pub fn parse<'a>( 139 parser: &mut TokenParser<'a>, 140 deck_author: &Pubkey, 141 ) -> Result<Self, ParseError<'a>> { 142 let tlkind = 143 parser.try_parse(|p| Ok(Route::Timeline(TimelineKind::parse(p, deck_author)?))); 144 145 if tlkind.is_ok() { 146 return tlkind; 147 } 148 149 TokenParser::alt( 150 parser, 151 &[ 152 |p| Ok(Route::Accounts(AccountsRoute::parse_from_tokens(p)?)), 153 |p| Ok(Route::AddColumn(AddColumnRoute::parse_from_tokens(p)?)), 154 |p| { 155 p.parse_all(|p| { 156 p.parse_token("deck")?; 157 p.parse_token("edit")?; 158 let ind_str = p.pull_token()?; 159 let parsed_index = ind_str 160 .parse::<usize>() 161 .map_err(|_| ParseError::DecodeFailed)?; 162 Ok(Route::EditDeck(parsed_index)) 163 }) 164 }, 165 |p| { 166 p.parse_all(|p| { 167 p.parse_token("profile")?; 168 p.parse_token("edit")?; 169 let pubkey = Pubkey::from_hex(p.pull_token()?) 170 .map_err(|_| ParseError::HexDecodeFailed)?; 171 Ok(Route::EditProfile(pubkey)) 172 }) 173 }, 174 |p| { 175 p.parse_all(|p| { 176 p.parse_token("relay")?; 177 Ok(Route::Relays) 178 }) 179 }, 180 |p| { 181 p.parse_all(|p| { 182 p.parse_token("settings")?; 183 Ok(Route::Settings) 184 }) 185 }, 186 |p| { 187 p.parse_all(|p| { 188 p.parse_token("quote")?; 189 Ok(Route::Quote(NoteId::new(tokenator::parse_hex_id(p)?))) 190 }) 191 }, 192 |p| { 193 p.parse_all(|p| { 194 p.parse_token("reply")?; 195 Ok(Route::Reply(NoteId::new(tokenator::parse_hex_id(p)?))) 196 }) 197 }, 198 |p| { 199 p.parse_all(|p| { 200 p.parse_token("compose")?; 201 Ok(Route::ComposeNote) 202 }) 203 }, 204 |p| { 205 p.parse_all(|p| { 206 p.parse_token("support")?; 207 Ok(Route::Support) 208 }) 209 }, 210 |p| { 211 p.parse_all(|p| { 212 p.parse_token("deck")?; 213 p.parse_token("new")?; 214 Ok(Route::NewDeck) 215 }) 216 }, 217 |p| { 218 p.parse_all(|p| { 219 p.parse_token("search")?; 220 Ok(Route::Search) 221 }) 222 }, 223 |p| { 224 p.parse_all(|p| { 225 p.parse_token("thread")?; 226 p.parse_token("root")?; 227 228 let root = tokenator::parse_hex_id(p)?; 229 230 p.parse_token("reply")?; 231 232 let selected = tokenator::parse_hex_id(p)?; 233 234 Ok(Route::Thread(ThreadSelection { 235 root_id: RootNoteIdBuf::new_unsafe(root), 236 selected_note: Some(NoteId::new(selected)), 237 })) 238 }) 239 }, 240 |p| { 241 p.parse_all(|p| { 242 p.parse_token("thread")?; 243 Ok(Route::Thread(ThreadSelection::from_root_id( 244 RootNoteIdBuf::new_unsafe(tokenator::parse_hex_id(p)?), 245 ))) 246 }) 247 }, 248 ], 249 ) 250 } 251 252 pub fn title(&self, i18n: &mut Localization) -> ColumnTitle<'_> { 253 match self { 254 Route::Timeline(kind) => kind.to_title(i18n), 255 Route::Thread(_) => { 256 ColumnTitle::formatted(tr!(i18n, "Thread", "Column title for note thread view")) 257 } 258 Route::Reply(_id) => { 259 ColumnTitle::formatted(tr!(i18n, "Reply", "Column title for reply composition")) 260 } 261 Route::Quote(_id) => { 262 ColumnTitle::formatted(tr!(i18n, "Quote", "Column title for quote composition")) 263 } 264 Route::Relays => { 265 ColumnTitle::formatted(tr!(i18n, "Relays", "Column title for relay management")) 266 } 267 Route::Settings => { 268 ColumnTitle::formatted(tr!(i18n, "Settings", "Column title for app settings")) 269 } 270 Route::Accounts(amr) => match amr { 271 AccountsRoute::Accounts => ColumnTitle::formatted(tr!( 272 i18n, 273 "Accounts", 274 "Column title for account management" 275 )), 276 AccountsRoute::AddAccount => ColumnTitle::formatted(tr!( 277 i18n, 278 "Add Account", 279 "Column title for adding new account" 280 )), 281 }, 282 Route::ComposeNote => ColumnTitle::formatted(tr!( 283 i18n, 284 "Compose Note", 285 "Column title for note composition" 286 )), 287 Route::AddColumn(c) => match c { 288 AddColumnRoute::Base => ColumnTitle::formatted(tr!( 289 i18n, 290 "Add Column", 291 "Column title for adding new column" 292 )), 293 AddColumnRoute::Algo(r) => match r { 294 AddAlgoRoute::Base => ColumnTitle::formatted(tr!( 295 i18n, 296 "Add Algo Column", 297 "Column title for adding algorithm column" 298 )), 299 AddAlgoRoute::LastPerPubkey => ColumnTitle::formatted(tr!( 300 i18n, 301 "Add Last Notes Column", 302 "Column title for adding last notes column" 303 )), 304 }, 305 AddColumnRoute::UndecidedNotification => ColumnTitle::formatted(tr!( 306 i18n, 307 "Add Notifications Column", 308 "Column title for adding notifications column" 309 )), 310 AddColumnRoute::ExternalNotification => ColumnTitle::formatted(tr!( 311 i18n, 312 "Add External Notifications Column", 313 "Column title for adding external notifications column" 314 )), 315 AddColumnRoute::Hashtag => ColumnTitle::formatted(tr!( 316 i18n, 317 "Add Hashtag Column", 318 "Column title for adding hashtag column" 319 )), 320 AddColumnRoute::UndecidedIndividual => ColumnTitle::formatted(tr!( 321 i18n, 322 "Subscribe to someone's notes", 323 "Column title for subscribing to individual user" 324 )), 325 AddColumnRoute::ExternalIndividual => ColumnTitle::formatted(tr!( 326 i18n, 327 "Subscribe to someone else's notes", 328 "Column title for subscribing to external user" 329 )), 330 }, 331 Route::Support => { 332 ColumnTitle::formatted(tr!(i18n, "Damus Support", "Column title for support page")) 333 } 334 Route::NewDeck => { 335 ColumnTitle::formatted(tr!(i18n, "Add Deck", "Column title for adding new deck")) 336 } 337 Route::EditDeck(_) => { 338 ColumnTitle::formatted(tr!(i18n, "Edit Deck", "Column title for editing deck")) 339 } 340 Route::EditProfile(_) => ColumnTitle::formatted(tr!( 341 i18n, 342 "Edit Profile", 343 "Column title for profile editing" 344 )), 345 Route::Search => { 346 ColumnTitle::formatted(tr!(i18n, "Search", "Column title for search page")) 347 } 348 Route::Wallet(_) => { 349 ColumnTitle::formatted(tr!(i18n, "Wallet", "Column title for wallet management")) 350 } 351 Route::CustomizeZapAmount(_) => ColumnTitle::formatted(tr!( 352 i18n, 353 "Customize Zap Amount", 354 "Column title for zap amount customization" 355 )), 356 } 357 } 358 } 359 360 // TODO: add this to egui-nav so we don't have to deal with returning 361 // and navigating headaches 362 #[derive(Clone, Debug)] 363 pub struct Router<R: Clone> { 364 routes: Vec<R>, 365 pub returning: bool, 366 pub navigating: bool, 367 replacing: bool, 368 369 // An overlay captures a range of routes where only one will persist when going back, the most recent added 370 overlay_ranges: Vec<Range<usize>>, 371 } 372 373 impl<R: Clone> Router<R> { 374 pub fn new(routes: Vec<R>) -> Self { 375 if routes.is_empty() { 376 panic!("routes can't be empty") 377 } 378 let returning = false; 379 let navigating = false; 380 let replacing = false; 381 Router { 382 routes, 383 returning, 384 navigating, 385 replacing, 386 overlay_ranges: Vec::new(), 387 } 388 } 389 390 pub fn route_to(&mut self, route: R) { 391 self.navigating = true; 392 self.routes.push(route); 393 } 394 395 pub fn route_to_overlaid(&mut self, route: R) { 396 self.route_to(route); 397 self.set_overlaying(); 398 } 399 400 pub fn route_to_overlaid_new(&mut self, route: R) { 401 self.route_to(route); 402 self.new_overlay(); 403 } 404 405 // Route to R. Then when it is successfully placed, should call `remove_previous_routes` to remove all previous routes 406 pub fn route_to_replaced(&mut self, route: R) { 407 self.navigating = true; 408 self.replacing = true; 409 self.routes.push(route); 410 } 411 412 /// Go back, start the returning process 413 pub fn go_back(&mut self) -> Option<R> { 414 if self.returning || self.routes.len() == 1 { 415 return None; 416 } 417 self.returning = true; 418 419 if let Some(range) = self.overlay_ranges.pop() { 420 tracing::debug!("Going back, found overlay: {:?}", range); 421 self.remove_overlay(range); 422 } else { 423 tracing::debug!("Going back, no overlay"); 424 } 425 426 if self.routes.len() == 1 { 427 return None; 428 } 429 430 self.prev().cloned() 431 } 432 433 /// Pop a route, should only be called on a NavRespose::Returned reseponse 434 pub fn pop(&mut self) -> Option<R> { 435 if self.routes.len() == 1 { 436 return None; 437 } 438 439 's: { 440 let Some(last_range) = self.overlay_ranges.last_mut() else { 441 break 's; 442 }; 443 444 if last_range.end != self.routes.len() { 445 break 's; 446 } 447 448 if last_range.end - 1 <= last_range.start { 449 self.overlay_ranges.pop(); 450 break 's; 451 } 452 453 last_range.end -= 1; 454 } 455 456 self.returning = false; 457 self.routes.pop() 458 } 459 460 pub fn remove_previous_routes(&mut self) { 461 let num_routes = self.routes.len(); 462 if num_routes <= 1 { 463 return; 464 } 465 466 self.returning = false; 467 self.replacing = false; 468 self.routes.drain(..num_routes - 1); 469 } 470 471 /// Removes all routes in the overlay besides the last 472 fn remove_overlay(&mut self, overlay_range: Range<usize>) { 473 let num_routes = self.routes.len(); 474 if num_routes <= 1 { 475 return; 476 } 477 478 if overlay_range.len() <= 1 { 479 return; 480 } 481 482 self.routes 483 .drain(overlay_range.start..overlay_range.end - 1); 484 } 485 486 pub fn is_replacing(&self) -> bool { 487 self.replacing 488 } 489 490 fn set_overlaying(&mut self) { 491 let mut overlaying_active = None; 492 let mut binding = self.overlay_ranges.last_mut(); 493 if let Some(range) = &mut binding { 494 if range.end == self.routes.len() - 1 { 495 overlaying_active = Some(range); 496 } 497 }; 498 499 if let Some(range) = overlaying_active { 500 range.end = self.routes.len(); 501 } else { 502 let new_range = self.routes.len() - 1..self.routes.len(); 503 self.overlay_ranges.push(new_range); 504 } 505 } 506 507 fn new_overlay(&mut self) { 508 let new_range = self.routes.len() - 1..self.routes.len(); 509 self.overlay_ranges.push(new_range); 510 } 511 512 pub fn top(&self) -> &R { 513 self.routes.last().expect("routes can't be empty") 514 } 515 516 pub fn prev(&self) -> Option<&R> { 517 self.routes.get(self.routes.len() - 2) 518 } 519 520 pub fn routes(&self) -> &Vec<R> { 521 &self.routes 522 } 523 } 524 525 /* 526 impl fmt::Display for Route { 527 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 528 match self { 529 Route::Timeline(kind) => match kind { 530 TimelineKind::List(ListKind::Contact(_pk)) => { 531 write!(f, "{}", i18n, "Home", "Display name for home feed")) 532 } 533 TimelineKind::Algo(AlgoTimeline::LastPerPubkey(ListKind::Contact(_))) => { 534 write!( 535 f, 536 "{}", 537 tr!( 538 "Last Per Pubkey (Contact)", 539 "Display name for last notes per contact" 540 ) 541 ) 542 } 543 TimelineKind::Notifications(_) => write!( 544 f, 545 "{}", 546 tr!("Notifications", "Display name for notifications") 547 ), 548 TimelineKind::Universe => { 549 write!(f, "{}", tr!("Universe", "Display name for universe feed")) 550 } 551 TimelineKind::Generic(_) => { 552 write!(f, "{}", tr!("Custom", "Display name for custom timelines")) 553 } 554 TimelineKind::Search(_) => { 555 write!(f, "{}", tr!("Search", "Display name for search results")) 556 } 557 TimelineKind::Hashtag(ht) => write!( 558 f, 559 "{} ({})", 560 tr!("Hashtags", "Display name for hashtag feeds"), 561 ht.join(" ") 562 ), 563 TimelineKind::Profile(_id) => { 564 write!(f, "{}", tr!("Profile", "Display name for user profiles")) 565 } 566 }, 567 Route::Thread(_) => write!(f, "{}", tr!("Thread", "Display name for thread view")), 568 Route::Reply(_id) => { 569 write!(f, "{}", tr!("Reply", "Display name for reply composition")) 570 } 571 Route::Quote(_id) => { 572 write!(f, "{}", tr!("Quote", "Display name for quote composition")) 573 } 574 Route::Relays => write!(f, "{}", tr!("Relays", "Display name for relay management")), 575 Route::Settings => write!(f, "{}", tr!("Settings", "Display name for settings management")), 576 Route::Accounts(amr) => match amr { 577 AccountsRoute::Accounts => write!( 578 f, 579 "{}", 580 tr!("Accounts", "Display name for account management") 581 ), 582 AccountsRoute::AddAccount => write!( 583 f, 584 "{}", 585 tr!("Add Account", "Display name for adding account") 586 ), 587 }, 588 Route::ComposeNote => write!( 589 f, 590 "{}", 591 tr!("Compose Note", "Display name for note composition") 592 ), 593 Route::AddColumn(_) => { 594 write!(f, "{}", tr!("Add Column", "Display name for adding column")) 595 } 596 Route::Support => write!(f, "{}", tr!("Support", "Display name for support page")), 597 Route::NewDeck => write!(f, "{}", tr!("Add Deck", "Display name for adding deck")), 598 Route::EditDeck(_) => { 599 write!(f, "{}", tr!("Edit Deck", "Display name for editing deck")) 600 } 601 Route::EditProfile(_) => write!( 602 f, 603 "{}", 604 tr!("Edit Profile", "Display name for profile editing") 605 ), 606 Route::Search => write!(f, "{}", tr!("Search", "Display name for search page")), 607 Route::Wallet(_) => { 608 write!(f, "{}", tr!("Wallet", "Display name for wallet management")) 609 } 610 Route::CustomizeZapAmount(_) => write!( 611 f, 612 "{}", 613 tr!("Customize Zap Amount", "Display name for zap customization") 614 ), 615 } 616 } 617 } 618 */ 619 620 #[derive(Clone, Debug)] 621 pub struct SingletonRouter<R: Clone> { 622 route: Option<R>, 623 pub returning: bool, 624 pub navigating: bool, 625 } 626 627 impl<R: Clone> SingletonRouter<R> { 628 pub fn route_to(&mut self, route: R) { 629 self.navigating = true; 630 self.route = Some(route); 631 } 632 633 pub fn go_back(&mut self) { 634 self.returning = true; 635 } 636 637 pub fn clear(&mut self) { 638 self.route = None; 639 self.returning = false; 640 } 641 642 pub fn route(&self) -> &Option<R> { 643 &self.route 644 } 645 } 646 647 impl<R: Clone> Default for SingletonRouter<R> { 648 fn default() -> Self { 649 Self { 650 route: None, 651 returning: false, 652 navigating: false, 653 } 654 } 655 } 656 657 #[cfg(test)] 658 mod tests { 659 use enostr::NoteId; 660 use tokenator::{TokenParser, TokenWriter}; 661 662 use crate::{timeline::ThreadSelection, Route}; 663 use enostr::Pubkey; 664 use notedeck::RootNoteIdBuf; 665 666 #[test] 667 fn test_thread_route_serialize() { 668 let note_id_hex = "1c54e5b0c386425f7e017d9e068ddef8962eb2ce1bb08ed27e24b93411c12e60"; 669 let note_id = NoteId::from_hex(note_id_hex).unwrap(); 670 let data_str = format!("thread:{}", note_id_hex); 671 let data = &data_str.split(":").collect::<Vec<&str>>(); 672 let mut token_writer = TokenWriter::default(); 673 let mut parser = TokenParser::new(&data); 674 let parsed = Route::parse(&mut parser, &Pubkey::new(*note_id.bytes())).unwrap(); 675 let expected = Route::Thread(ThreadSelection::from_root_id(RootNoteIdBuf::new_unsafe( 676 *note_id.bytes(), 677 ))); 678 parsed.serialize_tokens(&mut token_writer); 679 assert_eq!(expected, parsed); 680 assert_eq!(token_writer.str(), data_str); 681 } 682 }