notedeck

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

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(&note_id.hex());
    100             }
    101             Route::Quote(note_id) => {
    102                 writer.write_token("quote");
    103                 writer.write_token(&note_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 }