notedeck

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

nav.rs (18599B)


      1 use crate::{
      2     accounts::render_accounts_route,
      3     actionbar::NoteAction,
      4     app::{get_active_columns_mut, get_decks_mut},
      5     column::ColumnsAction,
      6     deck_state::DeckState,
      7     decks::{Deck, DecksAction, DecksCache},
      8     profile::{ProfileAction, SaveProfileChanges},
      9     profile_state::ProfileState,
     10     relay_pool_manager::RelayPoolManager,
     11     route::Route,
     12     timeline::{route::render_timeline_route, TimelineCache},
     13     ui::{
     14         self,
     15         add_column::render_add_column_routes,
     16         column::NavTitle,
     17         configure_deck::ConfigureDeckView,
     18         edit_deck::{EditDeckResponse, EditDeckView},
     19         note::{PostAction, PostType},
     20         profile::EditProfileView,
     21         search::SearchView,
     22         support::SupportView,
     23         RelayView, View,
     24     },
     25     Damus,
     26 };
     27 
     28 use egui_nav::{Nav, NavAction, NavResponse, NavUiType};
     29 use nostrdb::Transaction;
     30 use notedeck::{AccountsAction, AppContext};
     31 use tracing::error;
     32 
     33 #[allow(clippy::enum_variant_names)]
     34 pub enum RenderNavAction {
     35     Back,
     36     RemoveColumn,
     37     PostAction(PostAction),
     38     NoteAction(NoteAction),
     39     ProfileAction(ProfileAction),
     40     SwitchingAction(SwitchingAction),
     41 }
     42 
     43 pub enum SwitchingAction {
     44     Accounts(AccountsAction),
     45     Columns(ColumnsAction),
     46     Decks(crate::decks::DecksAction),
     47 }
     48 
     49 impl SwitchingAction {
     50     /// process the action, and return whether switching occured
     51     pub fn process(
     52         &self,
     53         timeline_cache: &mut TimelineCache,
     54         decks_cache: &mut DecksCache,
     55         ctx: &mut AppContext<'_>,
     56     ) -> bool {
     57         match &self {
     58             SwitchingAction::Accounts(account_action) => match account_action {
     59                 AccountsAction::Switch(switch_action) => {
     60                     ctx.accounts.select_account(switch_action.switch_to);
     61                     // pop nav after switch
     62                     if let Some(src) = switch_action.source {
     63                         get_active_columns_mut(ctx.accounts, decks_cache)
     64                             .column_mut(src)
     65                             .router_mut()
     66                             .go_back();
     67                     }
     68                 }
     69                 AccountsAction::Remove(index) => ctx.accounts.remove_account(*index),
     70             },
     71             SwitchingAction::Columns(columns_action) => match *columns_action {
     72                 ColumnsAction::Remove(index) => {
     73                     let kinds_to_pop =
     74                         get_active_columns_mut(ctx.accounts, decks_cache).delete_column(index);
     75                     for kind in &kinds_to_pop {
     76                         if let Err(err) = timeline_cache.pop(kind, ctx.ndb, ctx.pool) {
     77                             error!("error popping timeline: {err}");
     78                         }
     79                     }
     80                 }
     81 
     82                 ColumnsAction::Switch(from, to) => {
     83                     get_active_columns_mut(ctx.accounts, decks_cache).move_col(from, to);
     84                 }
     85             },
     86             SwitchingAction::Decks(decks_action) => match *decks_action {
     87                 DecksAction::Switch(index) => {
     88                     get_decks_mut(ctx.accounts, decks_cache).set_active(index)
     89                 }
     90                 DecksAction::Removing(index) => {
     91                     get_decks_mut(ctx.accounts, decks_cache).remove_deck(index)
     92                 }
     93             },
     94         }
     95         true
     96     }
     97 }
     98 
     99 impl From<PostAction> for RenderNavAction {
    100     fn from(post_action: PostAction) -> Self {
    101         Self::PostAction(post_action)
    102     }
    103 }
    104 
    105 impl From<NoteAction> for RenderNavAction {
    106     fn from(note_action: NoteAction) -> RenderNavAction {
    107         Self::NoteAction(note_action)
    108     }
    109 }
    110 
    111 pub type NotedeckNavResponse = NavResponse<Option<RenderNavAction>>;
    112 
    113 pub struct RenderNavResponse {
    114     column: usize,
    115     response: NotedeckNavResponse,
    116 }
    117 
    118 impl RenderNavResponse {
    119     #[allow(private_interfaces)]
    120     pub fn new(column: usize, response: NotedeckNavResponse) -> Self {
    121         RenderNavResponse { column, response }
    122     }
    123 
    124     #[must_use = "Make sure to save columns if result is true"]
    125     pub fn process_render_nav_response(&self, app: &mut Damus, ctx: &mut AppContext<'_>) -> bool {
    126         let mut switching_occured: bool = false;
    127         let col = self.column;
    128 
    129         if let Some(action) = self
    130             .response
    131             .response
    132             .as_ref()
    133             .or(self.response.title_response.as_ref())
    134         {
    135             // start returning when we're finished posting
    136             match action {
    137                 RenderNavAction::Back => {
    138                     app.columns_mut(ctx.accounts)
    139                         .column_mut(col)
    140                         .router_mut()
    141                         .go_back();
    142                 }
    143 
    144                 RenderNavAction::RemoveColumn => {
    145                     let kinds_to_pop = app.columns_mut(ctx.accounts).delete_column(col);
    146 
    147                     for kind in &kinds_to_pop {
    148                         if let Err(err) = app.timeline_cache.pop(kind, ctx.ndb, ctx.pool) {
    149                             error!("error popping timeline: {err}");
    150                         }
    151                     }
    152 
    153                     switching_occured = true;
    154                 }
    155 
    156                 RenderNavAction::PostAction(post_action) => {
    157                     let txn = Transaction::new(ctx.ndb).expect("txn");
    158                     let _ = post_action.execute(ctx.ndb, &txn, ctx.pool, &mut app.drafts);
    159                     get_active_columns_mut(ctx.accounts, &mut app.decks_cache)
    160                         .column_mut(col)
    161                         .router_mut()
    162                         .go_back();
    163                 }
    164 
    165                 RenderNavAction::NoteAction(note_action) => {
    166                     let txn = Transaction::new(ctx.ndb).expect("txn");
    167 
    168                     note_action.execute_and_process_result(
    169                         ctx.ndb,
    170                         get_active_columns_mut(ctx.accounts, &mut app.decks_cache),
    171                         col,
    172                         &mut app.timeline_cache,
    173                         ctx.note_cache,
    174                         ctx.pool,
    175                         &txn,
    176                         ctx.unknown_ids,
    177                     );
    178                 }
    179 
    180                 RenderNavAction::SwitchingAction(switching_action) => {
    181                     switching_occured = switching_action.process(
    182                         &mut app.timeline_cache,
    183                         &mut app.decks_cache,
    184                         ctx,
    185                     );
    186                 }
    187                 RenderNavAction::ProfileAction(profile_action) => {
    188                     profile_action.process(
    189                         &mut app.view_state.pubkey_to_profile_state,
    190                         ctx.ndb,
    191                         ctx.pool,
    192                         get_active_columns_mut(ctx.accounts, &mut app.decks_cache)
    193                             .column_mut(col)
    194                             .router_mut(),
    195                     );
    196                 }
    197             }
    198         }
    199 
    200         if let Some(action) = self.response.action {
    201             match action {
    202                 NavAction::Returned => {
    203                     let r = app
    204                         .columns_mut(ctx.accounts)
    205                         .column_mut(col)
    206                         .router_mut()
    207                         .pop();
    208 
    209                     if let Some(Route::Timeline(kind)) = &r {
    210                         if let Err(err) = app.timeline_cache.pop(kind, ctx.ndb, ctx.pool) {
    211                             error!("popping timeline had an error: {err} for {:?}", kind);
    212                         }
    213                     };
    214 
    215                     switching_occured = true;
    216                 }
    217 
    218                 NavAction::Navigated => {
    219                     let cur_router = app.columns_mut(ctx.accounts).column_mut(col).router_mut();
    220                     cur_router.navigating = false;
    221                     if cur_router.is_replacing() {
    222                         cur_router.remove_previous_routes();
    223                     }
    224                     switching_occured = true;
    225                 }
    226 
    227                 NavAction::Dragging => {}
    228                 NavAction::Returning => {}
    229                 NavAction::Resetting => {}
    230                 NavAction::Navigating => {}
    231             }
    232         }
    233 
    234         switching_occured
    235     }
    236 }
    237 
    238 fn render_nav_body(
    239     ui: &mut egui::Ui,
    240     app: &mut Damus,
    241     ctx: &mut AppContext<'_>,
    242     top: &Route,
    243     depth: usize,
    244     col: usize,
    245     inner_rect: egui::Rect,
    246 ) -> Option<RenderNavAction> {
    247     match top {
    248         Route::Timeline(kind) => render_timeline_route(
    249             ctx.ndb,
    250             ctx.img_cache,
    251             ctx.unknown_ids,
    252             ctx.note_cache,
    253             &mut app.timeline_cache,
    254             ctx.accounts,
    255             kind,
    256             col,
    257             app.note_options,
    258             depth,
    259             ui,
    260         ),
    261 
    262         Route::Accounts(amr) => {
    263             let mut action = render_accounts_route(
    264                 ui,
    265                 ctx.ndb,
    266                 col,
    267                 ctx.img_cache,
    268                 ctx.accounts,
    269                 &mut app.decks_cache,
    270                 &mut app.view_state.login,
    271                 *amr,
    272             );
    273             let txn = Transaction::new(ctx.ndb).expect("txn");
    274             action.process_action(ctx.unknown_ids, ctx.ndb, &txn);
    275             action
    276                 .accounts_action
    277                 .map(|f| RenderNavAction::SwitchingAction(SwitchingAction::Accounts(f)))
    278         }
    279 
    280         Route::Relays => {
    281             let manager = RelayPoolManager::new(ctx.pool);
    282             RelayView::new(ctx.accounts, manager, &mut app.view_state.id_string_map).ui(ui);
    283             None
    284         }
    285 
    286         Route::Reply(id) => {
    287             let txn = if let Ok(txn) = Transaction::new(ctx.ndb) {
    288                 txn
    289             } else {
    290                 ui.label("Reply to unknown note");
    291                 return None;
    292             };
    293 
    294             let note = if let Ok(note) = ctx.ndb.get_note_by_id(&txn, id.bytes()) {
    295                 note
    296             } else {
    297                 ui.label("Reply to unknown note");
    298                 return None;
    299             };
    300 
    301             let id = egui::Id::new(("post", col, note.key().unwrap()));
    302             let poster = ctx.accounts.selected_or_first_nsec()?;
    303 
    304             let action = {
    305                 let draft = app.drafts.reply_mut(note.id());
    306 
    307                 let response = egui::ScrollArea::vertical().show(ui, |ui| {
    308                     ui::PostReplyView::new(
    309                         ctx.ndb,
    310                         poster,
    311                         draft,
    312                         ctx.note_cache,
    313                         ctx.img_cache,
    314                         &note,
    315                         inner_rect,
    316                         app.note_options,
    317                     )
    318                     .id_source(id)
    319                     .show(ui)
    320                 });
    321 
    322                 response.inner.action
    323             };
    324 
    325             action.map(Into::into)
    326         }
    327 
    328         Route::Quote(id) => {
    329             let txn = Transaction::new(ctx.ndb).expect("txn");
    330 
    331             let note = if let Ok(note) = ctx.ndb.get_note_by_id(&txn, id.bytes()) {
    332                 note
    333             } else {
    334                 ui.label("Quote of unknown note");
    335                 return None;
    336             };
    337 
    338             let id = egui::Id::new(("post", col, note.key().unwrap()));
    339 
    340             let poster = ctx.accounts.selected_or_first_nsec()?;
    341             let draft = app.drafts.quote_mut(note.id());
    342 
    343             let response = egui::ScrollArea::vertical().show(ui, |ui| {
    344                 crate::ui::note::QuoteRepostView::new(
    345                     ctx.ndb,
    346                     poster,
    347                     ctx.note_cache,
    348                     ctx.img_cache,
    349                     draft,
    350                     &note,
    351                     inner_rect,
    352                     app.note_options,
    353                 )
    354                 .id_source(id)
    355                 .show(ui)
    356             });
    357 
    358             response.inner.action.map(Into::into)
    359         }
    360 
    361         Route::ComposeNote => {
    362             let kp = ctx.accounts.get_selected_account()?.to_full()?;
    363             let draft = app.drafts.compose_mut();
    364 
    365             let txn = Transaction::new(ctx.ndb).expect("txn");
    366             let post_response = ui::PostView::new(
    367                 ctx.ndb,
    368                 draft,
    369                 PostType::New,
    370                 ctx.img_cache,
    371                 ctx.note_cache,
    372                 kp,
    373                 inner_rect,
    374                 app.note_options,
    375             )
    376             .ui(&txn, ui);
    377 
    378             post_response.action.map(Into::into)
    379         }
    380 
    381         Route::AddColumn(route) => {
    382             render_add_column_routes(ui, app, ctx, col, route);
    383 
    384             None
    385         }
    386 
    387         Route::Support => {
    388             SupportView::new(&mut app.support).show(ui);
    389             None
    390         }
    391 
    392         Route::Search => {
    393             let id = ui.id().with(("search", depth, col));
    394             let search_buffer = app.view_state.searches.entry(id).or_default();
    395             let txn = Transaction::new(ctx.ndb).expect("txn");
    396 
    397             SearchView::new(
    398                 ctx.ndb,
    399                 &txn,
    400                 ctx.note_cache,
    401                 ctx.img_cache,
    402                 &ctx.accounts.mutefun(),
    403                 app.note_options,
    404                 search_buffer,
    405             )
    406             .show(ui)
    407             .map(RenderNavAction::NoteAction)
    408         }
    409 
    410         Route::NewDeck => {
    411             let id = ui.id().with("new-deck");
    412             let new_deck_state = app.view_state.id_to_deck_state.entry(id).or_default();
    413             let mut resp = None;
    414             if let Some(config_resp) = ConfigureDeckView::new(new_deck_state).ui(ui) {
    415                 if let Some(cur_acc) = ctx.accounts.get_selected_account() {
    416                     app.decks_cache.add_deck(
    417                         cur_acc.pubkey,
    418                         Deck::new(config_resp.icon, config_resp.name),
    419                     );
    420 
    421                     // set new deck as active
    422                     let cur_index = get_decks_mut(ctx.accounts, &mut app.decks_cache)
    423                         .decks()
    424                         .len()
    425                         - 1;
    426                     resp = Some(RenderNavAction::SwitchingAction(SwitchingAction::Decks(
    427                         DecksAction::Switch(cur_index),
    428                     )));
    429                 }
    430 
    431                 new_deck_state.clear();
    432                 get_active_columns_mut(ctx.accounts, &mut app.decks_cache)
    433                     .get_first_router()
    434                     .go_back();
    435             }
    436             resp
    437         }
    438         Route::EditDeck(index) => {
    439             let mut action = None;
    440             let cur_deck = get_decks_mut(ctx.accounts, &mut app.decks_cache)
    441                 .decks_mut()
    442                 .get_mut(*index)
    443                 .expect("index wasn't valid");
    444             let id = ui.id().with((
    445                 "edit-deck",
    446                 ctx.accounts.get_selected_account().map(|k| k.pubkey),
    447                 index,
    448             ));
    449             let deck_state = app
    450                 .view_state
    451                 .id_to_deck_state
    452                 .entry(id)
    453                 .or_insert_with(|| DeckState::from_deck(cur_deck));
    454             if let Some(resp) = EditDeckView::new(deck_state).ui(ui) {
    455                 match resp {
    456                     EditDeckResponse::Edit(configure_deck_response) => {
    457                         cur_deck.edit(configure_deck_response);
    458                     }
    459                     EditDeckResponse::Delete => {
    460                         action = Some(RenderNavAction::SwitchingAction(SwitchingAction::Decks(
    461                             DecksAction::Removing(*index),
    462                         )));
    463                     }
    464                 }
    465                 get_active_columns_mut(ctx.accounts, &mut app.decks_cache)
    466                     .get_first_router()
    467                     .go_back();
    468             }
    469 
    470             action
    471         }
    472         Route::EditProfile(pubkey) => {
    473             let mut action = None;
    474             if let Some(kp) = ctx.accounts.get_full(pubkey.bytes()) {
    475                 let state = app
    476                     .view_state
    477                     .pubkey_to_profile_state
    478                     .entry(*kp.pubkey)
    479                     .or_insert_with(|| {
    480                         let txn = Transaction::new(ctx.ndb).expect("txn");
    481                         if let Ok(record) = ctx.ndb.get_profile_by_pubkey(&txn, kp.pubkey.bytes()) {
    482                             ProfileState::from_profile(&record)
    483                         } else {
    484                             ProfileState::default()
    485                         }
    486                     });
    487                 if EditProfileView::new(state, ctx.img_cache).ui(ui) {
    488                     if let Some(taken_state) =
    489                         app.view_state.pubkey_to_profile_state.remove(kp.pubkey)
    490                     {
    491                         action = Some(RenderNavAction::ProfileAction(ProfileAction::SaveChanges(
    492                             SaveProfileChanges::new(kp.to_full(), taken_state),
    493                         )))
    494                     }
    495                 }
    496             } else {
    497                 error!("Pubkey in EditProfile route did not have an nsec attached in Accounts");
    498             }
    499             action
    500         }
    501     }
    502 }
    503 
    504 #[must_use = "RenderNavResponse must be handled by calling .process_render_nav_response(..)"]
    505 pub fn render_nav(
    506     col: usize,
    507     inner_rect: egui::Rect,
    508     app: &mut Damus,
    509     ctx: &mut AppContext<'_>,
    510     ui: &mut egui::Ui,
    511 ) -> RenderNavResponse {
    512     let nav_response = Nav::new(
    513         &app.columns(ctx.accounts)
    514             .column(col)
    515             .router()
    516             .routes()
    517             .clone(),
    518     )
    519     .navigating(
    520         app.columns_mut(ctx.accounts)
    521             .column_mut(col)
    522             .router_mut()
    523             .navigating,
    524     )
    525     .returning(
    526         app.columns_mut(ctx.accounts)
    527             .column_mut(col)
    528             .router_mut()
    529             .returning,
    530     )
    531     .id_source(egui::Id::new(("nav", col)))
    532     .show_mut(ui, |ui, render_type, nav| match render_type {
    533         NavUiType::Title => NavTitle::new(
    534             ctx.ndb,
    535             ctx.img_cache,
    536             get_active_columns_mut(ctx.accounts, &mut app.decks_cache),
    537             nav.routes(),
    538             col,
    539         )
    540         .show(ui),
    541         NavUiType::Body => {
    542             if let Some(top) = nav.routes().last() {
    543                 render_nav_body(ui, app, ctx, top, nav.routes().len(), col, inner_rect)
    544             } else {
    545                 None
    546             }
    547         }
    548     });
    549 
    550     RenderNavResponse::new(col, nav_response)
    551 }